Skip to content

Commit 8109696

Browse files
authored
Merge pull request #115 from Materials-Data-Science-and-Informatics/feature/poetryv2
Feature/poetryv2
2 parents 4722dc3 + b76147b commit 8109696

14 files changed

Lines changed: 367 additions & 98 deletions

File tree

.somesy.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "somesy"
3-
version = "0.6.0"
3+
version = "0.7.0"
44
description = "A CLI tool for synchronizing software project metadata."
55
keywords = ["metadata", "FAIR"]
66
license = "MIT"

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,14 @@ Here we provide notes that summarize the most important changes in each released
44

55
Please consult the changelog to inform yourself about breaking changes and security issues.
66

7-
## [v0.7.0](https://github.com/Materials-Data-Science-and-Informatics/somesy/tree/v0.7.0) <small>(2025-xx-xx)</small> { id="0.7.0" }
7+
## [v0.7.0](https://github.com/Materials-Data-Science-and-Informatics/somesy/tree/v0.7.0) <small>(2025-03-04)</small> { id="0.7.0" }
88

99
- make validation of output files, such as pyproject.toml, optional
1010
- make somesy project metadata input `version` optional
1111
- multiple output file support
1212
- enable having packages support
1313
- fix: package.json url set error on None value
14+
- support poetry version 2
1415

1516
## [v0.6.0](https://github.com/Materials-Data-Science-and-Informatics/somesy/tree/v0.6.0) <small>(2025-02-14)</small> { id="0.6.0" }
1617

CITATION.cff

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ type: software
33
message: If you use this software, please cite it using this metadata.
44

55
title: somesy
6-
version: 0.6.0
6+
version: 0.7.0
77
abstract: A CLI tool for synchronizing software project metadata.
88
url: https://materials-data-science-and-informatics.github.io/somesy
99
repository-code: https://github.com/Materials-Data-Science-and-Informatics/somesy

README.md

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,10 @@ Starting with version **0.3.0**, `somesy` supports Linux, MacOS and Windows.
5656

5757
Make sure that you use the latest version in order to avoid any problems.
5858

59+
! info
60+
61+
Poetry changed location of its project metadata with its version 2. Starting with version **0.7.0**, `somesy` supports both major versions of `poetry`, version 1 and 2.
62+
5963
### Installing somesy
6064

6165
Somesy requires Python `>=3.8`. To get a first impression, you can install the
@@ -172,7 +176,7 @@ file in the root folder of your repository:
172176
repos:
173177
# ... (your other hooks) ...
174178
- repo: https://github.com/Materials-Data-Science-and-Informatics/somesy
175-
rev: 'v0.6.0'
179+
rev: 'v0.7.0'
176180
hooks:
177181
- id: somesy
178182
```
@@ -193,18 +197,18 @@ so when using `somesy` with pre-commit, keep in mind that
193197

194198
Here is an overview of all the currently supported files and formats.
195199

196-
| Input Formats | Status | | Target Formats | Status |
197-
| -------------- | ------ | --- | ----------------------------- | ------ |
198-
| (.)somesy.toml | ✓ | | pyproject.toml _(poetry)_ | ✓ |
199-
| pyproject.toml | ✓ | | pyproject.toml _(setuptools)_ | ✓(1.) |
200-
| package.json | ✓ | | package.json _(JavaScript)_ | ✓(2.) |
201-
| Project.toml | ✓ | | Project.toml _(Julia)_ | ✓ |
202-
| fpm.toml | ✓ | | fpm.toml _(Fortran)_ | ✓(3.) |
203-
| | ✓ | | pom.toml _(Java)_ | ✓(4.) |
204-
| Cargo.toml | ✓ | | Cargo.toml _(Rust)_ | ✓ |
205-
| | | | mkdocs.yml | ✓(5.) |
206-
| | | | CITATION.cff | ✓ |
207-
| | | | codemeta.json | ✓(6.) |
200+
| Input Formats | Status | | Target Formats | Status |
201+
| -------------- | ------ | --- | ----------------------------------- | ------ |
202+
| (.)somesy.toml | ✓ | | pyproject.toml _(poetry v1 and v2)_ | ✓ |
203+
| pyproject.toml | ✓ | | pyproject.toml _(setuptools)_ | ✓(1.) |
204+
| package.json | ✓ | | package.json _(JavaScript)_ | ✓(2.) |
205+
| Project.toml | ✓ | | Project.toml _(Julia)_ | ✓ |
206+
| fpm.toml | ✓ | | fpm.toml _(Fortran)_ | ✓(3.) |
207+
| | ✓ | | pom.toml _(Java)_ | ✓(4.) |
208+
| Cargo.toml | ✓ | | Cargo.toml _(Rust)_ | ✓ |
209+
| | | | mkdocs.yml | ✓(5.) |
210+
| | | | CITATION.cff | ✓ |
211+
| | | | codemeta.json | ✓(6.) |
208212

209213
**Notes:**
210214

codemeta.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
],
2828
"name": "somesy",
2929
"description": "A CLI tool for synchronizing software project metadata.",
30-
"version": "0.6.0",
30+
"version": "0.7.0",
3131
"keywords": [
3232
"metadata",
3333
"FAIR"

docs/manual.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,7 @@ one of the supported input formats:
225225
=== "pyproject.toml"
226226

227227
```toml
228-
[tool.poetry]
228+
[tool.poetry] # [project] in case of poetry version 2
229229
name = "my-amazing-project"
230230
version = "0.1.0"
231231
...

pyproject.toml

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,29 @@
1-
[tool.poetry]
1+
[project]
22
name = "somesy"
3-
version = "0.6.0"
3+
version = "0.7.0"
44
description = "A CLI tool for synchronizing software project metadata."
5-
authors = ["Mustafa Soylu <m.soylu@fz-juelich.de>", "Anton Pirogov <a.pirogov@fz-juelich.de>"]
6-
maintainers = ["Mustafa Soylu <m.soylu@fz-juelich.de>"]
75
license = "MIT"
8-
96
readme = "README.md"
10-
repository = "https://github.com/Materials-Data-Science-and-Informatics/somesy"
11-
homepage = "https://materials-data-science-and-informatics.github.io/somesy"
12-
documentation = "https://materials-data-science-and-informatics.github.io/somesy"
13-
147
keywords = ["metadata", "FAIR"]
158
classifiers = [
169
"Operating System :: POSIX :: Linux",
10+
"Operating System :: MacOS :: MacOS X",
11+
"Operating System :: Microsoft :: Windows",
12+
"Natural Language :: English",
1713
"Intended Audience :: Developers",
1814
"Intended Audience :: Science/Research",
1915
"Topic :: Software Development :: Libraries :: Application Frameworks",
2016
"License :: OSI Approved :: MIT License",
2117
]
2218

19+
authors = [{name = "Mustafa Soylu", email = "m.soylu@fz-juelich.de"}, {name = "Anton Pirogov", email = "a.pirogov@fz-juelich.de"}]
20+
maintainers = [{name = "Mustafa Soylu", email = "m.soylu@fz-juelich.de"}]
21+
[project.urls]
22+
homepage = "https://materials-data-science-and-informatics.github.io/somesy"
23+
repository = "https://github.com/Materials-Data-Science-and-Informatics/somesy"
24+
documentation = "https://materials-data-science-and-informatics.github.io/somesy"
25+
26+
[tool.poetry]
2327
# the Python packages that will be included in a built distribution:
2428
packages = [{include = "somesy", from = "src"}]
2529

src/somesy/core/writer.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,10 @@ def _sync_person_list(
251251
252252
"""
253253
old_people: List[Union[Person, Entity]] = self._parse_people(old)
254+
if old_people is None or len(old_people) == 0:
255+
return new
256+
if new is None or len(new) == 0:
257+
return old_people
254258
return self._merge_person_metadata(old_people, new)
255259

256260
def _sync_authors(self, metadata: ProjectMetadata) -> None:
@@ -259,7 +263,10 @@ def _sync_authors(self, metadata: ProjectMetadata) -> None:
259263
This method is existing for the publication_author special case
260264
when synchronizing to CITATION.cff.
261265
"""
262-
self.authors = self._sync_person_list(self.authors, metadata.authors())
266+
if self.authors is None or len(self.authors) == 0:
267+
self.authors = metadata.authors()
268+
else:
269+
self.authors = self._sync_person_list(self.authors, metadata.authors())
263270

264271
def sync(self, metadata: ProjectMetadata) -> None:
265272
"""Sync output file with other metadata files."""
@@ -301,7 +308,7 @@ def _parse_people(cls, people: Optional[List[Any]]) -> List[Union[Person, Entity
301308
# remove None values
302309
people = [p for p in people if p is not None]
303310

304-
people = list(map(cls._to_person, people or []))
311+
people = list(map(lambda p: cls._to_person(p), people or []))
305312
return people
306313

307314
# ----
@@ -345,7 +352,7 @@ def description(self, description: str) -> None:
345352
def authors(self):
346353
"""Return the authors of the project."""
347354
authors = self._get_property(self._get_key("authors"))
348-
if authors is None:
355+
if authors is None or len(authors) == 0:
349356
return []
350357

351358
# only return authors that can be converted to Person

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: 89 additions & 15 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
@@ -104,26 +104,55 @@ def __init__(
104104
self,
105105
path: Path,
106106
pass_validation: Optional[bool] = False,
107+
version: Optional[int] = 1,
107108
):
108109
"""Poetry config file handler parsed from pyproject.toml.
109110
110111
See [somesy.core.writer.ProjectMetadataWriter.__init__][].
111112
"""
112-
super().__init__(
113-
path,
114-
section=["tool", "poetry"],
115-
model_cls=PoetryConfig,
116-
pass_validation=pass_validation,
117-
)
113+
self._poetry_version = version
114+
v2_mappings = {
115+
"homepage": ["urls", "homepage"],
116+
"repository": ["urls", "repository"],
117+
"documentation": ["urls", "documentation"],
118+
}
119+
if version == 1:
120+
super().__init__(
121+
path,
122+
section=["tool", "poetry"],
123+
model_cls=PoetryConfig,
124+
pass_validation=pass_validation,
125+
)
126+
else:
127+
super().__init__(
128+
path,
129+
section=["project"],
130+
model_cls=PoetryConfig,
131+
pass_validation=pass_validation,
132+
direct_mappings=v2_mappings,
133+
)
118134

119135
@staticmethod
120-
def _from_person(person: Union[Person, Entity]):
136+
def _from_person(person: Union[Person, Entity], poetry_version: int = 1):
121137
"""Convert project metadata person object to poetry string for person format "full name <email>."""
122-
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
123145

124146
@staticmethod
125-
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]]:
126150
"""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
127156
try:
128157
return Person.from_name_email_string(person)
129158
except (ValueError, AttributeError):
@@ -135,6 +164,41 @@ def _to_person(person: str) -> Optional[Union[Person, Entity]]:
135164
logger.warning(f"Cannot convert {person} to Entity.")
136165
return None
137166

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+
138202

139203
class SetupTools(PyprojectCommon):
140204
"""Setuptools config file handler parsed from setup.cfg."""
@@ -230,14 +294,24 @@ def __init__(self, path: Path, pass_validation: Optional[bool] = False):
230294
data = load(f)
231295

232296
# inspect file to pick suitable project metadata writer
233-
if "project" in data:
297+
is_poetry = "tool" in data and "poetry" in data["tool"]
298+
has_project = "project" in data
299+
300+
if is_poetry:
301+
if has_project:
302+
logger.verbose(
303+
"Found Poetry 2.x metadata with project section in pyproject.toml"
304+
)
305+
else:
306+
logger.verbose("Found Poetry 1.x metadata in pyproject.toml")
307+
self.__wrapped__ = Poetry(
308+
path, pass_validation=pass_validation, version=2 if has_project else 1
309+
)
310+
elif has_project and not is_poetry:
234311
logger.verbose("Found setuptools-based metadata in pyproject.toml")
235312
self.__wrapped__ = SetupTools(path, pass_validation=pass_validation)
236-
elif "tool" in data and "poetry" in data["tool"]:
237-
logger.verbose("Found poetry-based metadata in pyproject.toml")
238-
self.__wrapped__ = Poetry(path, pass_validation=pass_validation)
239313
else:
240-
msg = "The pyproject.toml file is ambiguous, either add a [project] or [tool.poetry] section"
314+
msg = "The pyproject.toml file is ambiguous. For Poetry projects, ensure [tool.poetry] section exists. For setuptools, ensure [project] section exists without [tool.poetry]"
241315
raise ValueError(msg)
242316

243317
super().__init__(self.__wrapped__)

0 commit comments

Comments
 (0)