Skip to content

Commit 8d371cd

Browse files
committed
fix: use correct poetry v2 person format
1 parent dac2b26 commit 8d371cd

4 files changed

Lines changed: 114 additions & 33 deletions

File tree

src/somesy/pyproject/models.py

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,13 @@
2424
logger = getLogger("somesy")
2525

2626

27+
class STPerson(BaseModel):
28+
"""Person model for setuptools."""
29+
30+
name: Annotated[str, Field(min_length=1)]
31+
email: Annotated[Optional[str], Field(min_length=1)] = None
32+
33+
2734
class PoetryConfig(BaseModel):
2835
"""Poetry configuration model."""
2936

@@ -45,10 +52,13 @@ class PoetryConfig(BaseModel):
4552
Optional[Union[LicenseEnum, List[LicenseEnum]]],
4653
Field(description="An SPDX license identifier."),
4754
]
48-
authors: Annotated[Set[str], Field(description="Package authors")]
55+
56+
# v1 has str, v2 has STPerson
57+
authors: Annotated[List[Union[str, STPerson]], Field(description="Package authors")]
4958
maintainers: Annotated[
50-
Optional[Set[str]], Field(description="Package maintainers")
59+
Optional[List[Union[str, STPerson]]], Field(description="Package maintainers")
5160
] = None
61+
5262
readme: Annotated[
5363
Optional[Union[Path, List[Path]]], Field(description="Package readme file(s)")
5464
] = None
@@ -90,10 +100,17 @@ def validate_email_format(cls, v):
90100
validated = []
91101
for author in v:
92102
try:
93-
if not (
94-
not isinstance(author, str)
95-
or " " not in author
96-
or not EMailAddress.validate_python(author.split(" ")[-1][1:-1])
103+
if isinstance(author, STPerson) and author.email:
104+
if not EMailAddress.validate_python(author.email):
105+
logger.warning(
106+
f"Invalid email format for author/maintainer {author}, omitting."
107+
)
108+
else:
109+
validated.append(author)
110+
continue
111+
112+
if " " in author and EMailAddress.validate_python(
113+
author.split(" ")[-1][1:-1]
97114
):
98115
validated.append(author)
99116
else:
@@ -150,13 +167,6 @@ def validate_xor(cls, values):
150167
return values
151168

152169

153-
class STPerson(BaseModel):
154-
"""Person model for setuptools."""
155-
156-
name: Annotated[str, Field(min_length=1)]
157-
email: Annotated[Optional[str], Field(min_length=1)] = None
158-
159-
160170
class URLs(BaseModel):
161171
"""URLs model for setuptools."""
162172

src/somesy/pyproject/writer.py

Lines changed: 71 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import logging
44
from pathlib import Path
5-
from typing import Any, List, Optional, Union
5+
from typing import Any, Dict, List, Optional, Union
66

77
import tomlkit
88
import wrapt
@@ -110,25 +110,49 @@ def __init__(
110110
111111
See [somesy.core.writer.ProjectMetadataWriter.__init__][].
112112
"""
113+
self._poetry_version = version
114+
v2_mappings = {
115+
"homepage": ["urls", "homepage"],
116+
"repository": ["urls", "repository"],
117+
"documentation": ["urls", "documentation"],
118+
}
113119
if version == 1:
114-
section = ["tool", "poetry"]
120+
super().__init__(
121+
path,
122+
section=["tool", "poetry"],
123+
model_cls=PoetryConfig,
124+
pass_validation=pass_validation,
125+
)
115126
else:
116-
section = ["project"]
117-
super().__init__(
118-
path,
119-
section=section,
120-
model_cls=PoetryConfig,
121-
pass_validation=pass_validation,
122-
)
127+
super().__init__(
128+
path,
129+
section=["project"],
130+
model_cls=PoetryConfig,
131+
pass_validation=pass_validation,
132+
direct_mappings=v2_mappings,
133+
)
123134

124135
@staticmethod
125-
def _from_person(person: Union[Person, Entity]):
136+
def _from_person(person: Union[Person, Entity], poetry_version: int = 1):
126137
"""Convert project metadata person object to poetry string for person format "full name <email>."""
127-
return person.to_name_email_string()
138+
if poetry_version == 1:
139+
return person.to_name_email_string()
140+
else:
141+
response = {"name": person.full_name}
142+
if person.email:
143+
response["email"] = person.email
144+
return response
128145

129146
@staticmethod
130-
def _to_person(person: str) -> Optional[Union[Person, Entity]]:
147+
def _to_person(
148+
person: Union[str, Dict[str, str]],
149+
) -> Optional[Union[Person, Entity]]:
131150
"""Convert from free string to person or entity object."""
151+
if isinstance(person, dict):
152+
temp = str(person["name"])
153+
if "email" in person:
154+
temp = f"{temp} <{person['email']}>"
155+
person = temp
132156
try:
133157
return Person.from_name_email_string(person)
134158
except (ValueError, AttributeError):
@@ -140,6 +164,41 @@ def _to_person(person: str) -> Optional[Union[Person, Entity]]:
140164
logger.warning(f"Cannot convert {person} to Entity.")
141165
return None
142166

167+
def sync(self, metadata: ProjectMetadata) -> None:
168+
"""Sync metadata with pyproject.toml file."""
169+
# Store original _from_person method
170+
original_from_person = self._from_person
171+
172+
# Override _from_person to include poetry_version
173+
self._from_person = lambda person: original_from_person( # type: ignore
174+
person, poetry_version=self._poetry_version
175+
)
176+
177+
# Call parent sync method
178+
super().sync(metadata)
179+
180+
# Restore original _from_person method
181+
self._from_person = original_from_person # type: ignore
182+
183+
# For Poetry v2, convert authors and maintainers from array of tables to inline tables
184+
if self._poetry_version == 2:
185+
for field in ["authors", "maintainers"]:
186+
field_value = self._get_property([field])
187+
if field_value:
188+
# Create an inline array of tables
189+
inline_array = tomlkit.array()
190+
inline_array.multiline(False)
191+
192+
# Convert each table to an inline table and add to array
193+
for item in field_value:
194+
inline_table = tomlkit.inline_table()
195+
for k, v in item.items():
196+
inline_table[k] = v
197+
inline_array.append(inline_table)
198+
199+
# Replace the array of tables with the inline array
200+
self._set_property(field, inline_array)
201+
143202

144203
class SetupTools(PyprojectCommon):
145204
"""Setuptools config file handler parsed from setup.cfg."""

tests/data/pyproject2.toml

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,8 @@
22
name = "test-package"
33
version = "0.1.0"
44
description = "This is a test package for demonstration purposes."
5-
authors = ["John Doe <john.doe@example.com>"]
5+
authors = [{ "name" = "John Doe", "email" = "john.doe@example.com" }]
66
license = "MIT"
7-
repository = "https://github.com/example/test-package"
8-
homepage = "https://example.com/test-package"
9-
documentation = "https://example.com/test-package"
107
keywords = ["test", "demo", "example"]
118
classifiers = [
129
"Operating System :: POSIX :: Linux",
@@ -16,6 +13,11 @@ classifiers = [
1613
"Topic :: Software Development :: Libraries :: Application Frameworks",
1714
]
1815

16+
[project.urls]
17+
homepage = "https://example.com/test-package"
18+
repository = "https://github.com/example/test-package"
19+
documentation = "https://example.com/test-package"
20+
1921
[tool.poetry.dependencies]
2022
python = "^3.8"
2123
pydantic = "^1.10.2"

tests/output/test_pyproject_writer.py

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -166,8 +166,12 @@ def test_person_merge_pyproject(
166166
pj.save()
167167

168168
# existing author info preserved, order not preserved because no orcid
169-
person1b_rep = writer_class._from_person(person1b)
170-
person2_rep = writer_class._from_person(person2)
169+
if version is not None:
170+
person1b_rep = writer_class._from_person(person1b, poetry_version=version)
171+
person2_rep = writer_class._from_person(person2, poetry_version=version)
172+
else:
173+
person1b_rep = writer_class._from_person(person1b)
174+
person2_rep = writer_class._from_person(person2)
171175
assert (pj.authors[0] == person1b_rep) or (pj.authors[1] == person1b_rep)
172176
assert (pj.authors[0] == person2_rep) or (pj.authors[1] == person2_rep)
173177

@@ -181,11 +185,17 @@ def test_person_merge_pyproject(
181185
"publication_author": True,
182186
}
183187
)
184-
person3_rep = writer_class._from_person(person3)
188+
if version is not None:
189+
person3_rep = writer_class._from_person(person3, poetry_version=version)
190+
else:
191+
person3_rep = writer_class._from_person(person3)
185192

186193
# john has a new email address
187194
person1c = person1b.model_copy(update={"email": "john.of.us@qualityland.com"})
188-
person1c_rep = writer_class._from_person(person1c)
195+
if version is not None:
196+
person1c_rep = writer_class._from_person(person1c, poetry_version=version)
197+
else:
198+
person1c_rep = writer_class._from_person(person1c)
189199

190200
# jane 2 is removed from authors, but added to maintainers
191201
person2.author = False

0 commit comments

Comments
 (0)