Skip to content

Commit 5760d73

Browse files
author
a.pirogov
committed
feat: add documentation URL, which is supported by some formats
1 parent 6276896 commit 5760d73

15 files changed

Lines changed: 153 additions & 90 deletions

File tree

.gitignore

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
# don't add custom python shell scratchpad file
2-
scratch*.py
3-
scratch
2+
scratch*
43

54
# generated badges
65
docs/*_badge.svg

.somesy.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ keywords = ["metadata", "FAIR"]
66
license = "MIT"
77
repository = "https://github.com/Materials-Data-Science-and-Informatics/somesy"
88
homepage = "https://materials-data-science-and-informatics.github.io/somesy"
9+
documentation = "https://materials-data-science-and-informatics.github.io/somesy"
910

1011
[[project.people]]
1112
family-names = "Soylu"

CHANGELOG.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@
33
Here we provide notes that summarize the most important changes in each released version.
44

55
Please consult the changelog to inform yourself about breaking changes and security issues.
6-
## [Unreleased] - yyyy-mm-dd
76

8-
### Added
9-
* added julia support for `Project.toml` file
7+
## [v0.4.0](https://github.com/Materials-Data-Science-and-Informatics/somesy/tree/v0.4.0) <small>(2024-02-??)</small> { id="0.4.0" }
8+
9+
* added separate `documentation` URL to Project metadata model
10+
* added support for Julia `Project.toml` file
1011

1112
## [v0.3.1](https://github.com/Materials-Data-Science-and-Informatics/somesy/tree/v0.3.1) <small>(2024-01-23)</small> { id="0.3.1" }
1213

CITATION.cff

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ message: If you use this software, please cite it using this metadata.
55
title: somesy
66
version: 0.3.1
77
abstract: A CLI tool for synchronizing software project metadata.
8+
url: https://materials-data-science-and-informatics.github.io/somesy
89
repository-code: https://github.com/Materials-Data-Science-and-Informatics/somesy
910
license: MIT
1011
keywords:
@@ -32,4 +33,3 @@ contact:
3233
family-names: Soylu
3334
given-names: Mustafa
3435
orcid: https://orcid.org/0000-0003-2637-0432
35-
url: https://materials-data-science-and-informatics.github.io/somesy

codemeta.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
],
4545
"softwareHelp": "https://materials-data-science-and-informatics.github.io/somesy",
4646
"codeRepository": "https://github.com/Materials-Data-Science-and-Informatics/somesy",
47+
"buildInstructions": "https://materials-data-science-and-informatics.github.io/somesy",
4748
"contributor": [
4849
{
4950
"@type": "Person",

docs/manual.md

Lines changed: 25 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -113,34 +113,37 @@ print(model2md(SomesyConfig).getvalue())
113113

114114
From its own schema `somesy` must convert the information into the target formats.
115115
The following tables sketch how fields are mapped to corresponding other fields in
116-
some of the currently supported formats.
116+
some of the currently supported formats. Bold field names are mandatory, the others are optional.
117117

118118
=== "Person Metadata"
119119

120-
| Field Name | Poetry Config | SetupTools Config | CITATION.cff | package.json | Requirement |
121-
| ---------------- | ------------- | ----------------- | --------------- | ------------ | ----------- |
122-
| given-names | name+email | name | given-names | name | required |
123-
| family-names | name+email | name | family-names | name | required |
124-
| email | name+email | email | email | email | required |
125-
| orcid | - | - | orcid | url | optional |
126-
| *(many others)* | - | - | *(same)* | - | optional |
120+
| Somesy Field | Poetry Config | SetupTools Config | Julia Config | package.json | CITATION.cff | CodeMeta |
121+
| ---------------- | ------------- | ----------------- | ------------ | ------------ | --------------- | --------------- |
122+
| | | | | | | |
123+
| **given-names** | name+email | name | name+email | name | given-names | givenName |
124+
| **family-names** | name+email | name | name+email | name | family-names | familyName |
125+
| **email** | name+email | email | name+email | email | email | email |
126+
| orcid | - | - | - | url | orcid | id |
127+
| *(many others)* | - | - | - | - | *(same)* | *(same)* |
127128

128129
=== "Project Metadata"
129130

130-
| Field Name | Poetry Config | SetupTools Config | Julia Config || CITATION.cff | package.json | Requirement |
131-
| ----------------- | ------------- | ----------------- | ------------ || --------------- | ------------ | ----------- |
132-
| name | name | name | name || title | name | required |
133-
| description | description | description | - || abstract | description | required |
134-
| license | license | license | - || license | license | required |
135-
| version | version | version | version || version | version | optional |
136-
| | | | || | | |
137-
| *author=true* | authors | authors | authors || authors | author | required |
138-
| *maintainer=true* | maintainers | maintainers | - || contact | maintainers | optional |
139-
| *people* | - | - | - || - | contributors | optional |
140-
| | | | || | | |
141-
| keywords | keywords | keywords | - || keywords | keywords | optional |
142-
| repository | repository | urls.repository | = || repository_code | repository | optional |
143-
| homepage | homepage | urls.homepage | - || url | homepage | optional |
131+
| Somesy Field | Poetry Config | SetupTools Config | Julia Config | package.json | CITATION.cff | CodeMeta |
132+
| ----------------- | ------------- | ------------------ | ------------ | ------------ | --------------- | ----------------- |
133+
| | | | | | |
134+
| **name** | name | name | name | name | title | name |
135+
| **description** | description | description | - | description | abstract | description |
136+
| **license** | license | license | - | license | license | license |
137+
| **version** | version | version | version | version | version | version |
138+
| | | | | | | |
139+
| ***author=true*** | authors | authors | authors | author | authors | author |
140+
| *maintainer=true* | maintainers | maintainers | - | maintainers | contact | maintainer |
141+
| *people* | - | - | - | contributors | - | contributor |
142+
| | | | | | | |
143+
| keywords | keywords | keywords | - | keywords | keywords | keywords |
144+
| homepage | homepage | urls.homepage | - | homepage | url | url |
145+
| repository | repository | urls.repository | - | repository | repository_code | codeRepository |
146+
| documentation | documentation | urls.documentation | - | - | - | buildInstructions |
144147

145148
Note that the mapping is often not 1-to-1. For example, CITATION.cff allows rich
146149
specification of author contact information and complex names. In contrast,

src/somesy/cff/writer.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from ruamel.yaml import YAML
88

99
from somesy.core.models import Person, ProjectMetadata
10-
from somesy.core.writer import ProjectMetadataWriter
10+
from somesy.core.writer import FieldKeyMapping, IgnoreKey, ProjectMetadataWriter
1111

1212

1313
class CFF(ProjectMetadataWriter):
@@ -25,11 +25,12 @@ def __init__(
2525
self._yaml = YAML()
2626
self._yaml.preserve_quotes = True
2727

28-
mappings = {
28+
mappings: FieldKeyMapping = {
2929
"name": ["title"],
3030
"description": ["abstract"],
3131
"homepage": ["url"],
3232
"repository": ["repository-code"],
33+
"documentation": IgnoreKey(),
3334
"maintainers": ["contact"],
3435
}
3536
super().__init__(

src/somesy/codemeta/writer.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from rich.pretty import pretty_repr
88

99
from somesy.core.models import Person, ProjectMetadata
10-
from somesy.core.writer import ProjectMetadataWriter
10+
from somesy.core.writer import FieldKeyMapping, ProjectMetadataWriter
1111
from somesy.json_wrapper import json
1212

1313
logger = logging.getLogger("somesy")
@@ -24,9 +24,10 @@ def __init__(
2424
2525
See [somesy.core.writer.ProjectMetadataWriter.__init__][].
2626
"""
27-
mappings = {
27+
mappings: FieldKeyMapping = {
2828
"repository": ["codeRepository"],
2929
"homepage": ["softwareHelp"],
30+
"documentation": ["buildInstructions"],
3031
"keywords": ["keywords"],
3132
"authors": ["author"],
3233
"maintainers": ["maintainer"],

src/somesy/core/models.py

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
import functools
55
import json
6+
import re
67
from datetime import date
78
from pathlib import Path
89
from typing import Any, Dict, List, Optional
@@ -357,6 +358,31 @@ def full_name(self) -> str:
357358

358359
return " ".join(names) if names else ""
359360

361+
def to_name_email_string(self) -> str:
362+
"""Convert project metadata person object to poetry string for person format `full name <x@y.z>`."""
363+
return f"{self.full_name} <{self.email}>"
364+
365+
@classmethod
366+
def from_name_email_string(cls, person: str) -> Person:
367+
"""Return a `Person` based on an name/e-mail string like `full name <x@y.z>`.
368+
369+
If the name is `A B C`, then `A B` will be the given names and `C` will be the family name.
370+
"""
371+
m = re.match(r"\s*([^<]+)<([^>]+)>", person)
372+
names, mail = (
373+
list(map(lambda s: s.strip(), m.group(1).split())),
374+
m.group(2).strip(),
375+
)
376+
# NOTE: for our purposes, does not matter what are given or family names,
377+
# we only compare on full_name anyway.
378+
return Person(
379+
**{
380+
"given-names": " ".join(names[:-1]),
381+
"family-names": names[-1],
382+
"email": mail,
383+
}
384+
)
385+
360386
def same_person(self, other) -> bool:
361387
"""Return whether two Person metadata records are about the same real person.
362388
@@ -413,12 +439,15 @@ def at_least_one_author(cls, people):
413439
version: Annotated[str, Field(description="Project version.")]
414440
license: Annotated[LicenseEnum, Field(description="SPDX License string.")]
415441

442+
homepage: Annotated[
443+
Optional[HttpUrlStr], Field(description="URL of the project homepage.")
444+
] = None
416445
repository: Annotated[
417446
Optional[HttpUrlStr],
418447
Field(description="URL of the project source code repository."),
419448
] = None
420-
homepage: Annotated[
421-
Optional[HttpUrlStr], Field(description="URL of the project homepage.")
449+
documentation: Annotated[
450+
Optional[HttpUrlStr], Field(description="URL of the project documentation.")
422451
] = None
423452

424453
keywords: Annotated[

src/somesy/core/writer.py

Lines changed: 55 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,14 @@
99
logger = logging.getLogger("somesy")
1010

1111

12+
class IgnoreKey:
13+
"""Special marker to be passed for dropping a key from serialization."""
14+
15+
16+
FieldKeyMapping = Dict[str, Union[List[str], IgnoreKey]]
17+
"""Type to be used for the dict passed as `direct_mappings`."""
18+
19+
1220
class ProjectMetadataWriter(ABC):
1321
"""Base class for Project Metadata Output Wrapper.
1422
@@ -20,7 +28,7 @@ def __init__(
2028
path: Path,
2129
*,
2230
create_if_not_exists: Optional[bool] = False,
23-
direct_mappings: Dict[str, List[str]] = None,
31+
direct_mappings: FieldKeyMapping = None,
2432
) -> None:
2533
"""Initialize the Project Metadata Output Wrapper.
2634
@@ -85,31 +93,54 @@ def save(self, path: Optional[Path]) -> None:
8593
without destroying its other contents or structure.
8694
"""
8795

88-
def _get_property(self, key: Union[str, List[str]]) -> Optional[Any]:
96+
def _get_property(
97+
self, key: Union[str, List[str]], *, remove: bool = False
98+
) -> Optional[Any]:
8999
"""Get a property from the data.
90100
91101
Override this to e.g. rewrite the retrieved key
92102
(e.g. if everything relevant is in some subobject).
103+
104+
Args:
105+
key: Name of the key or sequence of multiple keys to retrieve the value.
106+
remove: If True, will remove the retrieved value and clean up the dict.
93107
"""
94108
key_path = [key] if isinstance(key, str) else key
95109

96110
curr = self._data
111+
seq = [curr]
97112
for k in key_path:
98113
curr = curr.get(k)
114+
seq.append(curr)
99115
if curr is None:
100116
return None
101117

118+
if remove:
119+
seq.pop()
120+
logger.debug("remove in")
121+
logger.debug(seq[-1])
122+
del seq[-1][key_path[-1]] # remove leaf value
123+
# clean up the tree
124+
for key, dct in reversed(list(zip(key_path[:-1], seq[:-1]))):
125+
if not dct.get(key):
126+
del dct[key]
127+
102128
return curr
103129

104-
def _set_property(self, key: Union[str, List[str]], value: Any) -> None:
130+
def _set_property(self, key: Union[str, List[str], IgnoreKey], value: Any) -> None:
105131
"""Set a property in the data.
106132
107133
Override this to e.g. rewrite the retrieved key
108134
(e.g. if everything relevant is in some subobject).
109135
"""
110-
if not value:
136+
if isinstance(key, IgnoreKey):
111137
return
112138
key_path = [key] if isinstance(key, str) else key
139+
140+
if not value: # remove value and clean up the sub-dict
141+
self._get_property(key_path, remove=True)
142+
return
143+
113144
# create path on the fly if needed
114145
curr = self._data
115146
for key in key_path[:-1]:
@@ -220,10 +251,12 @@ def sync(self, metadata: ProjectMetadata) -> None:
220251
)
221252

222253
self.license = metadata.license.value
223-
if metadata.homepage:
224-
self.homepage = str(metadata.homepage)
225-
if metadata.repository:
226-
self.repository = str(metadata.repository)
254+
255+
self.homepage = str(metadata.homepage) if metadata.homepage else None
256+
self.repository = str(metadata.repository) if metadata.repository else None
257+
self.documentation = (
258+
str(metadata.documentation) if metadata.documentation else None
259+
)
227260

228261
@staticmethod
229262
@abstractmethod
@@ -326,16 +359,26 @@ def homepage(self) -> Optional[str]:
326359
return self._get_property(self._get_key("homepage"))
327360

328361
@homepage.setter
329-
def homepage(self, homepage: Optional[str]) -> None:
362+
def homepage(self, value: Optional[str]) -> None:
330363
"""Set the homepage url of the project."""
331-
self._set_property(self._get_key("homepage"), homepage)
364+
self._set_property(self._get_key("homepage"), value)
332365

333366
@property
334367
def repository(self) -> Optional[Union[str, dict]]:
335368
"""Return the repository url of the project."""
336369
return self._get_property(self._get_key("repository"))
337370

338371
@repository.setter
339-
def repository(self, repository: Optional[Union[str, dict]]) -> None:
372+
def repository(self, value: Optional[Union[str, dict]]) -> None:
340373
"""Set the repository url of the project."""
341-
self._set_property(self._get_key("repository"), repository)
374+
self._set_property(self._get_key("repository"), value)
375+
376+
@property
377+
def documentation(self) -> Optional[Union[str, dict]]:
378+
"""Return the documentation url of the project."""
379+
return self._get_property(self._get_key("documentation"))
380+
381+
@documentation.setter
382+
def documentation(self, value: Optional[Union[str, dict]]) -> None:
383+
"""Set the documentation url of the project."""
384+
self._set_property(self._get_key("documentation"), value)

0 commit comments

Comments
 (0)