Repository: DavidVujic/python-polylith

Issue:
Allow optional PEP 621 fields (authors, description)
**Describe the bug**
Currently, `authors` and `description` are expected to be present in the root `pyproject.toml`, and the contents from the fields are fetched when creating new projects with `poly create project`.

According to PEP 621, these fields are optional, but when set have to be according to a certain pattern.

More details: https://packaging.python.org/en/latest/specifications/pyproject-toml/#authors-maintainers

**To Reproduce**
Steps to reproduce the behavior:
1. run `poly create project --name helloworld` in a workspace with a `pyproject.toml` without `authors`.

**Error Log**

```shell
UnexpectedCharError: Unexpected character: '\n' at line 9 col 10
```

```shell
  ╭─────────────────────────── locals ────────────────────────────╮                                                                        │
│ │  found_equals = True                                          │                                                                        │
│ │        indent = ''                                            │                                                                        │
│ │           key = <Key authors >                                │                                                                        │
│ │ parse_comment = True                                          │                                                                        │
│ │          self = <tomlkit.parser.Parser object at 0x105af5760> │                                                                        │
│ ╰───────────────────────────────────────────────────────────────╯                                                                        │
│                                                                                                                                          │

│                                                                                                                                          │
│    537 │   │   │                                                                                                                         │
│    538 │   │   │   raise self.parse_error(InvalidNumberError)                                                                            │
│    539 │   │   else:                                                                                                                     │
│ ❱  540 │   │   │   raise self.parse_error(UnexpectedCharError, c)                                                                        │
│    541 │                                                                                                                                 │
│    542 │   def _parse_true(self):                                                                                                        │
│    543 │   │   return self._parse_bool(BoolType.TRUE)                                                                                    │
│                                                                                                                                          │
│ ╭───────────────────────────── locals ──────────────────────────────╮                                                                    │
│ │      c = '\n'                                                     │                                                                    │
│ │   self = <tomlkit.parser.Parser object at 0x105af5760>            │                                                                    │
│ │ trivia = Trivia(indent='', comment_ws='', comment='', trail='\n') │                                                                    │
│ ╰───────────────────────────────────────────────────────────────────╯ 
```

**Expected behavior**
The `poly` command should accept `authors` and `description` as optional fields.


Discussion:
sonarcloud[bot]: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=DavidVujic_python-polylith&pullRequest=260) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=DavidVujic_python-polylith&pullRequest=260&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=DavidVujic_python-polylith&pullRequest=260&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=DavidVujic_python-polylith&pullRequest=260&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=DavidVujic_python-polylith&pullRequest=260&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=DavidVujic_python-polylith&pullRequest=260&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=DavidVujic_python-polylith&pullRequest=260)


sonarcloud[bot]: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=DavidVujic_python-polylith&pullRequest=260) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=DavidVujic_python-polylith&pullRequest=260&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=DavidVujic_python-polylith&pullRequest=260&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=DavidVujic_python-polylith&pullRequest=260&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=DavidVujic_python-polylith&pullRequest=260&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=DavidVujic_python-polylith&pullRequest=260&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=DavidVujic_python-polylith&pullRequest=260)


sonarcloud[bot]: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=DavidVujic_python-polylith&pullRequest=260) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=DavidVujic_python-polylith&pullRequest=260&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=DavidVujic_python-polylith&pullRequest=260&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=DavidVujic_python-polylith&pullRequest=260&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=DavidVujic_python-polylith&pullRequest=260&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=DavidVujic_python-polylith&pullRequest=260&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=DavidVujic_python-polylith&pullRequest=260)



Patch:
diff --git a/components/polylith/project/create.py b/components/polylith/project/create.py
index 1a227d0..3629b4a 100644
--- a/components/polylith/project/create.py
+++ b/components/polylith/project/create.py
@@ -18,12 +18,15 @@ def create_project(path: Path, template: str, name: str, description: str) -> No
     authors = repo.get_authors(path)
     python_version = repo.get_python_version(path)
 
+    description_field = f'description = "{description}"' if description else ""
+    authors_field = f"authors = {authors}" if authors else ""
+
     project_toml = create_project_toml(
         template,
         {
             "name": name,
-            "description": description,
-            "authors": authors,
+            "description": description_field,
+            "authors": authors_field,
             "python_version": python_version,
         },
     )
diff --git a/components/polylith/project/templates.py b/components/polylith/project/templates.py
index ae03eec..695a93a 100644
--- a/components/polylith/project/templates.py
+++ b/components/polylith/project/templates.py
@@ -2,8 +2,8 @@ poetry_pyproject = """\
 [tool.poetry]
 name = "{name}"
 version = "0.1.0"
-description = "{description}"
-authors = {authors}
+{description}
+{authors}
 license = ""
 
 packages = []
@@ -24,8 +24,8 @@ build-backend = "hatchling.build"
 [project]
 name = "{name}"
 version = "0.1.0"
-description = "{description}"
-authors = {authors}
+{description}
+{authors}
 
 requires-python = "{python_version}"
 
@@ -44,8 +44,8 @@ build-backend = "pdm.backend"
 [project]
 name = "{name}"
 version = "0.1.0"
-description = "{description}"
-authors = {authors}
+{description}
+{authors}
 
 requires-python = "{python_version}"
 
diff --git a/projects/poetry_polylith_plugin/pyproject.toml b/projects/poetry_polylith_plugin/pyproject.toml
index c1b1947..2f6fe77 100644
--- a/projects/poetry_polylith_plugin/pyproject.toml
+++ b/projects/poetry_polylith_plugin/pyproject.toml
@@ -1,6 +1,6 @@
 [tool.poetry]
 name = "poetry-polylith-plugin"
-version = "1.27.5"
+version = "1.27.6"
 description = "A Poetry plugin that adds tooling support for the Polylith Architecture"
 authors = ["David Vujic"]
 homepage = "https://davidvujic.github.io/python-polylith-docs/"
diff --git a/projects/polylith_cli/pyproject.toml b/projects/polylith_cli/pyproject.toml
index 4084b7b..cc49c4d 100644
--- a/projects/polylith_cli/pyproject.toml
+++ b/projects/polylith_cli/pyproject.toml
@@ -1,6 +1,6 @@
 [tool.poetry]
 name = "polylith-cli"
-version = "1.15.1"
+version = "1.15.2"
 description = "Python tooling support for the Polylith Architecture"
 authors = ['David Vujic']
 homepage = "https://davidvujic.github.io/python-polylith-docs/"


Test patch:
diff --git a/test/components/polylith/project/test_templates.py b/test/components/polylith/project/test_templates.py
index 7984e0f..7a08eac 100644
--- a/test/components/polylith/project/test_templates.py
+++ b/test/components/polylith/project/test_templates.py
@@ -1,31 +1,93 @@
+import pytest
 import tomlkit
 from polylith.project import templates
 
-
 template_data = {
     "name": "a project",
-    "description": "a description",
-    "authors": [],
+    "description": "",
+    "authors": "",
     "python_version": "3.12",
 }
 
 
+def with_optionals(description_field: str, authors_field: str) -> dict:
+    return {
+        **template_data,
+        **{"description": description_field, "authors": authors_field},
+    }
+
+
+def with_poetry_optionals(description: str, author: str) -> dict:
+    description_field = f'description = "{description}"'
+    authors_field = f'authors = ["{author}"]'
+
+    return with_optionals(description_field, authors_field)
+
+
+def with_pep621_optionals(description: str, author: str) -> dict:
+    description_field = f'description = "{description}"'
+    authors_field = str.replace('authors = [{ name = "_AUTHOR_"}]', "_AUTHOR_", author)
+
+    return with_optionals(description_field, authors_field)
+
+
+def to_toml(template: str, data: dict):
+    return tomlkit.loads(template.format(**data))
+
+
 def test_poetry_template():
-    data = tomlkit.loads(templates.poetry_pyproject.format(**template_data))
+    data = to_toml(templates.poetry_pyproject, template_data)
 
     assert data["tool"]["poetry"] is not None
+    assert data["tool"]["poetry"].get("authors") is None
+    assert data["tool"]["poetry"].get("description") is None
+
+
+def test_poetry_template_with_optionals():
+    expected_description = "Hello world"
+    expected_author = "Unit test"
+
+    data = to_toml(
+        templates.poetry_pyproject,
+        with_poetry_optionals(expected_description, expected_author),
+    )
+
+    assert data["tool"]["poetry"]["description"] == expected_description
+    assert data["tool"]["poetry"]["authors"] == [expected_author]
 
 
 def test_hatch_template():
-    data = tomlkit.loads(templates.hatch_pyproject.format(**template_data))
+    data = to_toml(templates.hatch_pyproject, template_data)
 
     assert "hatch-polylith-bricks" in data["build-system"]["requires"]
     assert data["tool"]["hatch"]["build"]["hooks"]["polylith-bricks"] == {}
     assert data["tool"]["polylith"]["bricks"] == {}
 
+    assert data["project"].get("description") is None
+    assert data["project"].get("authors") is None
+
 
 def test_pdm_template():
-    data = tomlkit.loads(templates.pdm_pyproject.format(**template_data))
+    data = to_toml(templates.pdm_pyproject, template_data)
 
     assert "pdm-polylith-bricks" in data["build-system"]["requires"]
     assert data["tool"]["polylith"]["bricks"] == {}
+
+    assert data["project"].get("description") is None
+    assert data["project"].get("authors") is None
+
+
+@pytest.mark.parametrize("name", ["hatch", "pdm"])
+def test_pep621_template_with_optionals(name):
+    expected_description = "Hello world"
+    expected_author = "Unit Test"
+
+    template = {"hatch": templates.hatch_pyproject, "pdm": templates.pdm_pyproject}
+
+    data = to_toml(
+        template[name],
+        with_pep621_optionals(expected_description, expected_author),
+    )
+
+    assert data["project"]["description"] == expected_description
+    assert data["project"]["authors"] == [{"name": expected_author}]