Repository: python-poetry/poetry

Issue:
Poetry's internal pyproject.toml continually grows larger with empty lines
<!--
  Hi there! Thank you for discovering and submitting an issue.

  Before you submit this; let's make sure of a few things.
  Please make sure the following boxes are ticked if they are correct.
  If not, please try and fulfill these first.
-->

<!-- Checked checkbox should look like this: [x] -->
- [x] I am on the [latest](https://github.com/python-poetry/poetry/releases/latest) Poetry version.
- [x] I have searched the [issues](https://github.com/python-poetry/poetry/issues) of this repo and believe that this is not a duplicate.
- [x] If an exception occurs when executing a command, I executed it again in debug mode (`-vvv` option).

<!--
  Once those are done, if you're able to fill in the following list with your information,
  it'd be very helpful to whoever handles the issue.
-->

- **OS version and name**: Windows 11
- **Poetry version**: 1.2.0
- **Link of a [Gist](https://gist.github.com/) with the contents of your pyproject.toml file**: N/A

## Issue
<!-- Now feel free to write your issue, but please be descriptive! Thanks again 🙌 ❤️ -->

While I've been testing installing/uninstalling a plugin I wrote and running `poetry self add ...`, I've noticed that my `%APPDATA%/pypoetry/pyproject.toml` file is growing with more and more blank lines. Right now, there are 319,760 lines total. It looks like this:

```toml
[tool.poetry]
name = "poetry-instance"
version = "1.2.0"
description = ""
authors = []
license = ""

[tool.poetry.dependencies]
python = "3.10.5"
poetry = "1.2.0"

[tool.poetry.group.additional.dependencies]

# around 800 blank lines here

poetry-dynamic-versioning = {extras = ["plugin"], version = "^0.18.0"}





poetry-dynamic-versioning-plugin = "^0.4.0"

# then the rest  of the file is blank
```

### Testing from a blank slate
Delete Poetry's internal pyproject.toml and poetry.lock files.

Run: `poetry self add poetry-dynamic-versioning[plugin]`. Now pyproject.toml looks like:

```toml
[tool.poetry]
name = "poetry-instance"
version = "1.2.0"
description = ""
authors = []
license = ""

[tool.poetry.dependencies]
python = "3.10.5"
poetry = "1.2.0"

[tool.poetry.group.additional.dependencies]
poetry-dynamic-versioning = {extras = ["plugin"], version = "^0.18.0"}


```

Run: `poetry self remove poetry-dynamic-versioning`. Now pyproject.toml looks like:

```toml
[tool.poetry]
name = "poetry-instance"
version = "1.2.0"
description = ""
authors = []
license = ""

[tool.poetry.dependencies]
python = "3.10.5"
poetry = "1.2.0"

[tool.poetry.group.additional.dependencies]




```

Run: `poetry self add poetry-dynamic-versioning[plugin]`. Now pyproject.toml looks like:

```toml
[tool.poetry]
name = "poetry-instance"
version = "1.2.0"
description = ""
authors = []
license = ""

[tool.poetry.dependencies]
python = "3.10.5"
poetry = "1.2.0"

[tool.poetry.group.additional.dependencies]



poetry-dynamic-versioning = {extras = ["plugin"], version = "^0.18.0"}





```



Discussion:
Adding additional information from my duplicate issue:

My best guess is that the problem seems to be centered around how [generate_system_pyproject ](https://github.com/python-poetry/poetry/blob/20352b616a0bdabf6ee9f361afe35de24e40fb2b/src/poetry/console/commands/self/self_command.py#L60)touches and parses the lines and writes them back for the preserved keys of {"group", "source"}. It seems to go through Pathlib and uses the inherited write_text from pathlib.Path class.

Resolving patch:
diff --git a/src/poetry/console/commands/self/self_command.py b/src/poetry/console/commands/self/self_command.py
--- a/src/poetry/console/commands/self/self_command.py
+++ b/src/poetry/console/commands/self/self_command.py
@@ -77,7 +77,8 @@ def generate_system_pyproject(self) -> None:
         for key in preserved:
             content["tool"]["poetry"][key] = preserved[key]  # type: ignore[index]
 
-        self.system_pyproject.write_text(content.as_string(), encoding="utf-8")
+        pyproject = PyProjectTOML(self.system_pyproject)
+        pyproject.file.write(content)
 
     def reset_poetry(self) -> None:
         with directory(self.system_pyproject.parent):
diff --git a/src/poetry/factory.py b/src/poetry/factory.py
--- a/src/poetry/factory.py
+++ b/src/poetry/factory.py
@@ -190,9 +190,7 @@ def create_package_source(
         )
 
     @classmethod
-    def create_pyproject_from_package(
-        cls, package: Package, path: Path | None = None
-    ) -> TOMLDocument:
+    def create_pyproject_from_package(cls, package: Package) -> TOMLDocument:
         import tomlkit
 
         from poetry.utils.dependency_specification import dependency_to_specification
@@ -290,12 +288,6 @@ def create_pyproject_from_package(
             content["extras"] = extras_section
 
         pyproject = cast("TOMLDocument", pyproject)
-        pyproject.add(tomlkit.nl())
-
-        if path:
-            path.joinpath("pyproject.toml").write_text(
-                pyproject.as_string(), encoding="utf-8"
-            )
 
         return pyproject
 


Test patch:
diff --git a/tests/console/commands/self/test_self_command.py b/tests/console/commands/self/test_self_command.py
new file mode 100644
--- /dev/null
+++ b/tests/console/commands/self/test_self_command.py
@@ -0,0 +1,49 @@
+from __future__ import annotations
+
+import pytest
+
+from poetry.core.packages.dependency import Dependency
+from poetry.core.packages.package import Package
+from poetry.core.packages.project_package import ProjectPackage
+
+from poetry.__version__ import __version__
+from poetry.console.commands.self.self_command import SelfCommand
+from poetry.factory import Factory
+
+
+@pytest.fixture
+def example_system_pyproject():
+    package = ProjectPackage("poetry-instance", __version__)
+    plugin = Package("poetry-plugin", "1.2.3")
+
+    package.add_dependency(
+        Dependency(plugin.name, "^1.2.3", groups=[SelfCommand.ADDITIONAL_PACKAGE_GROUP])
+    )
+    content = Factory.create_pyproject_from_package(package)
+    return content.as_string().rstrip("\n")
+
+
+@pytest.mark.parametrize("existing_newlines", [0, 2])
+def test_generate_system_pyproject_trailing_newline(
+    existing_newlines: int,
+    example_system_pyproject: str,
+):
+    cmd = SelfCommand()
+    cmd.system_pyproject.write_text(example_system_pyproject + "\n" * existing_newlines)
+    cmd.generate_system_pyproject()
+    generated = cmd.system_pyproject.read_text()
+
+    assert len(generated) - len(generated.rstrip("\n")) == existing_newlines
+
+
+def test_generate_system_pyproject_carriage_returns(
+    example_system_pyproject: str,
+):
+    cmd = SelfCommand()
+    cmd.system_pyproject.write_text(example_system_pyproject + "\n")
+    cmd.generate_system_pyproject()
+
+    with open(cmd.system_pyproject, newline="") as f:  # do not translate newlines
+        generated = f.read()
+
+    assert "\r\r" not in generated