Repository: pdm-project/pdm

Issue:
pdm always crashes on a poetry project with names including comma
- [x] I have searched the issue tracker and believe that this is not a duplicate.

**Make sure you run commands with `-v` flag before pasting the output.**

## Steps to reproduce

On my test project with pyproject.toml of which name contains any comma(,), e.g.:

```toml
[tool.poetry]
name = "test"
version = "1.0.0"
descrption = "test project"
authors = ["Min-Gyu, Kim <alsrbbk@gmail.com>"]

[tool.poetry.dependencies]
python = "~3.11.6"
```

pdm command-line execution always crashes.

## Actual behavior

I got a main error when executing any CLI commands.

It seems to be due to a regex, unmatching commas.

Example as follows:

```sh
$ pdm
Traceback (most recent call last):
  File "/home/alsrbbk/.local/bin/pdm", line 8, in <module>
    sys.exit(main())
             ^^^^^^
  File "/home/alsrbbk/.local/share/pdm/venv/lib/python3.11/site-packages/pdm/core.py", line 288, in main
    return Core().main(args or sys.argv[1:])
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/alsrbbk/.local/share/pdm/venv/lib/python3.11/site-packages/pdm/core.py", line 181, in main
    args = self._get_cli_args(args, obj)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/alsrbbk/.local/share/pdm/venv/lib/python3.11/site-packages/pdm/core.py", line 163, in _get_cli_args
    config = project.pyproject.settings.get("options", {})
             ^^^^^^^^^^^^^^^^^
  File "/home/alsrbbk/.pyenv/versions/3.11.6/lib/python3.11/functools.py", line 1001, in __get__
    val = self.func(instance)
          ^^^^^^^^^^^^^^^^^^^
  File "/home/alsrbbk/.local/share/pdm/venv/lib/python3.11/site-packages/pdm/project/core.py", line 112, in pyproject
    return PyProject(self.root / self.PYPROJECT_FILENAME, ui=self.core.ui)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/alsrbbk/.local/share/pdm/venv/lib/python3.11/site-packages/pdm/project/toml_file.py", line 18, in __init__
    self._data = self.read()
                 ^^^^^^^^^^^
  File "/home/alsrbbk/.local/share/pdm/venv/lib/python3.11/site-packages/pdm/project/project_file.py", line 33, in read
    metadata, settings = converter.convert(None, self._path, None)
                         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/alsrbbk/.local/share/pdm/venv/lib/python3.11/site-packages/pdm/formats/poetry.py", line 213, in convert
    return converter.convert()
           ^^^^^^^^^^^^^^^^^^^
  File "/home/alsrbbk/.local/share/pdm/venv/lib/python3.11/site-packages/pdm/formats/base.py", line 85, in convert
    raise MetaConvertError(errors, data=self._data, settings=self.settings)
pdm.formats.base.MetaConvertError: 
authors: 'NoneType' object has no attribute 'groupdict'
```

## Expected behavior

`$ pdm` execution succeeded and show a help page.

## Environment Information

(cannot launch CLI)



Discussion:
According to the [specification](https://arc.net/l/quote/gvitpksa) the name can't contain commas, if I interpret it correctly
I assume the specification has very little to say about what the `tool.poetry` part of the file is allowed to contain

core metadata certainly seems to allow commas in the "author" field per the example https://packaging.python.org/en/latest/specifications/core-metadata/#core-metadata-author


Oh, so PDM reads and tries to convert the Poetry table on the fly :thinking:? I thought this would only happen when "importing" a project.

Regarding the spec:

- [PEP 621](https://peps.python.org/pep-0621/#authors-maintainers) does say that commas are forbidden in author/maintainer fields, probably because "Multiple values should be separated by commas".
- I'm not sure to understand why one would separate multiple values with commas when the field itself is an array.
- Furthermore, the [link](https://packaging.python.org/en/latest/specifications/core-metadata/#author) posted by @dimbleby shows that commas are accepted in the core metadata.
- The PEP is a marked as historical document, and the canonical spec is now https://packaging.python.org/en/latest/specifications/pyproject-toml/#pyproject-toml-spec, but it just repeats the same thing as the PEP for the author field.

Maybe worth asking on discuss.python.org.
Possibly related: https://discuss.python.org/t/clarification-regarding-the-authors-maintainers-field-in-pyproject-toml/33731
An email name needs to be non-comma according to the spec., so I have to change my name.

As @pawamoy said, PDM should not be crashed on a poetry project even if it contains an invalid name before any convertion. Maybe it can warn us to correct the name format. I had to spend a bit of time to figure out the reason.
This topic is related to https://github.com/pdm-project/pdm/issues/2651 and to state it clearly:
`,` in author name is most certainly supported in the spec (see https://packaging.python.org/en/latest/specifications/core-metadata/#core-metadata-author ), but is just bugged in `pdm`. It work fine with other tools and PyPI.

As for how it is possible for commas to be supported in name, if they are used for seperating values as well, the answer lies in https://packaging.python.org/en/latest/specifications/core-metadata/#author-email  -> RFC-822, assumes use of quotes to escape them.




Patch:
diff --git a/news/2665.bugfix.md b/news/2665.bugfix.md
new file mode 100644
index 0000000000..e1acc54de5
--- /dev/null
+++ b/news/2665.bugfix.md
@@ -0,0 +1,2 @@
+Fix errors when parsing poetry format that contains special characters in author name.
+Poetry-specific `parse_name_email` and `NAME_EMAIL_RE` moved from `pdm.formats.base` to `pdm.formats.poetry`.
diff --git a/src/pdm/formats/base.py b/src/pdm/formats/base.py
index af4fa72e3b..108ef06f57 100644
--- a/src/pdm/formats/base.py
+++ b/src/pdm/formats/base.py
@@ -1,6 +1,5 @@
 from __future__ import annotations
 
-import re
 from typing import Any, Callable, Mapping, TypeVar, cast
 
 import tomlkit
@@ -86,9 +85,6 @@ def convert(self) -> tuple[Mapping[str, Any], Mapping[str, Any]]:
         return self._data, self.settings
 
 
-NAME_EMAIL_RE = re.compile(r"(?P<name>[^,]+?)\s*(?:<(?P<email>.+)>)?\s*$")
-
-
 def make_inline_table(data: Mapping) -> dict:
     """Create an inline table from the given data."""
     table = cast(dict, tomlkit.inline_table())
@@ -106,16 +102,3 @@ def make_array(data: list, multiline: bool = False) -> list:
 
 def array_of_inline_tables(value: list[Mapping], multiline: bool = True) -> list[str]:
     return make_array([make_inline_table(item) for item in value], multiline)
-
-
-def parse_name_email(name_email: list[str]) -> list[str]:
-    return array_of_inline_tables(
-        [
-            {
-                k: v
-                for k, v in NAME_EMAIL_RE.match(item).groupdict().items()  # type: ignore[union-attr]
-                if v is not None
-            }
-            for item in name_email
-        ]
-    )
diff --git a/src/pdm/formats/poetry.py b/src/pdm/formats/poetry.py
index c05463e53d..440bb9cb83 100644
--- a/src/pdm/formats/poetry.py
+++ b/src/pdm/formats/poetry.py
@@ -11,10 +11,10 @@
 from pdm.formats.base import (
     MetaConverter,
     Unset,
+    array_of_inline_tables,
     convert_from,
     make_array,
     make_inline_table,
-    parse_name_email,
 )
 from pdm.models.markers import Marker, get_marker
 from pdm.models.requirements import Requirement
@@ -93,6 +93,27 @@ def _convert_req(name: str, req_dict: RequirementDict | list[RequirementDict]) -
         yield Requirement.from_req_dict(name, req_dict).as_line()
 
 
+# See https://github.com/python-poetry/poetry-core/pull/521#issuecomment-1327689551
+# for reasoning why email.utils.parseaddr is not used here.
+NAME_EMAIL_RE = re.compile(
+    r"^(?P<name>[- .,\w'’\"():&]+)(?: <(?P<email>.+?)>)?$",  # noqa: RUF001
+    re.UNICODE,
+)
+
+
+def parse_name_email(name_email: list[str]) -> list[str]:
+    return array_of_inline_tables(
+        [
+            {
+                k: v
+                for k, v in NAME_EMAIL_RE.match(item).groupdict().items()  # type: ignore[union-attr]
+                if v is not None
+            }
+            for item in name_email
+        ]
+    )
+
+
 class PoetryMetaConverter(MetaConverter):
     @convert_from("authors")
     def authors(self, value: list[str]) -> list[str]:


Test patch:
diff --git a/tests/fixtures/pyproject.toml b/tests/fixtures/pyproject.toml
index 03d7efca24..644fcbfce3 100644
--- a/tests/fixtures/pyproject.toml
+++ b/tests/fixtures/pyproject.toml
@@ -3,7 +3,8 @@ name = "poetry"
 version = "1.0.0"
 description = "Python dependency management and packaging made easy."
 authors = [
-    "Sébastien Eustace <sebastien@eustace.io>"
+    "Sébastien Eustace <sebastien@eustace.io>",
+    "Example, Inc. <inc@example.com>"
 ]
 license = "MIT"
 
diff --git a/tests/test_formats.py b/tests/test_formats.py
index 2ac06e85ab..c72f4db08f 100644
--- a/tests/test_formats.py
+++ b/tests/test_formats.py
@@ -77,10 +77,16 @@ def test_convert_poetry(project):
     with cd(FIXTURES):
         result, settings = poetry.convert(project, golden_file, ns())
 
-    assert result["authors"][0] == {
-        "name": "Sébastien Eustace",
-        "email": "sebastien@eustace.io",
-    }
+    assert result["authors"] == [
+        {
+            "name": "Sébastien Eustace",
+            "email": "sebastien@eustace.io",
+        },
+        {
+            "name": "Example, Inc.",
+            "email": "inc@example.com",
+        },
+    ]
     assert result["name"] == "poetry"
     assert result["version"] == "1.0.0"
     assert result["license"] == {"text": "MIT"}