Skip to content

Commit 8a11338

Browse files
committed
add julia module
1 parent f78ea63 commit 8a11338

3 files changed

Lines changed: 153 additions & 0 deletions

File tree

src/somesy/julia/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
"""Julia module."""
2+
from .writer import Julia
3+
4+
__all__ = ["Julia"]

src/somesy/julia/models.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
"""Julia model."""
2+
import uuid
3+
from typing import Optional, Set
4+
5+
from packaging.version import parse as parse_version
6+
from pydantic import (
7+
BaseModel,
8+
EmailStr,
9+
Field,
10+
TypeAdapter,
11+
field_validator,
12+
)
13+
from typing_extensions import Annotated
14+
15+
EMailAddress = TypeAdapter(EmailStr)
16+
17+
18+
class JuliaConfig(BaseModel):
19+
"""Julia configuration model."""
20+
21+
model_config = dict(use_enum_values=True)
22+
23+
name: Annotated[
24+
str,
25+
Field(description="Package name"),
26+
]
27+
version: Annotated[
28+
str,
29+
Field(
30+
pattern=r"^\d+(\.\d+)*((a|b|rc)\d+)?(post\d+)?(dev\d+)?$",
31+
description="Package version",
32+
),
33+
]
34+
uuid: Annotated[str, Field(description="Package UUID")]
35+
authors: Annotated[Optional[Set[str]], Field(description="Package authors")] = None
36+
37+
@field_validator("version")
38+
@classmethod
39+
def validate_version(cls, v):
40+
"""Validate version using PEP 440."""
41+
try:
42+
_ = parse_version(v)
43+
except ValueError as err:
44+
raise ValueError("Invalid version") from err
45+
return v
46+
47+
@field_validator("authors")
48+
@classmethod
49+
def validate_email_format(cls, v):
50+
"""Validate email format."""
51+
for author in v:
52+
if (
53+
not isinstance(author, str)
54+
or " " not in author
55+
or not EMailAddress.validate_python(author.split(" ")[-1][1:-1])
56+
):
57+
raise ValueError("Invalid email format")
58+
return v
59+
60+
@field_validator("uuid")
61+
@classmethod
62+
def validate_uuid(cls, v):
63+
"""Validate uuid field."""
64+
try:
65+
_ = uuid.UUID(v)
66+
except ValueError as err:
67+
raise ValueError("Invalid UUID") from err
68+
return v

src/somesy/julia/writer.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
"""Julia writer."""
2+
import logging
3+
import re
4+
from pathlib import Path
5+
from typing import Optional
6+
7+
import tomlkit
8+
from rich.pretty import pretty_repr
9+
10+
from somesy.core.models import Person, ProjectMetadata
11+
from somesy.core.writer import ProjectMetadataWriter
12+
13+
from .models import JuliaConfig
14+
15+
logger = logging.getLogger("somesy")
16+
17+
18+
class Julia(ProjectMetadataWriter):
19+
"""Julia config file handler parsed from Project.toml."""
20+
21+
def __init__(self, path: Path):
22+
"""Julia config file handler parsed from Project.toml.
23+
24+
See [somesy.core.writer.ProjectMetadataWriter.__init__][].
25+
"""
26+
super().__init__(path, create_if_not_exists=False)
27+
28+
def _load(self) -> None:
29+
"""Load Project.toml file."""
30+
with open(self.path) as f:
31+
self._data = tomlkit.load(f)
32+
33+
def _validate(self) -> None:
34+
"""Validate poetry config using pydantic class.
35+
36+
In order to preserve toml comments and structure, tomlkit library is used.
37+
Pydantic class only used for validation.
38+
"""
39+
config = dict(self._get_property([]))
40+
logger.debug(
41+
f"Validating config using {JuliaConfig.__name__}: {pretty_repr(config)}"
42+
)
43+
JuliaConfig(**config)
44+
45+
def save(self, path: Optional[Path] = None) -> None:
46+
"""Save the julia file."""
47+
path = path or self.path
48+
with open(path, "w") as f:
49+
tomlkit.dump(self._data, f)
50+
51+
@staticmethod
52+
def _from_person(person: Person):
53+
"""Convert project metadata person object to poetry string for person format "full name <email>."""
54+
return f"{person.full_name} <{person.email}>"
55+
56+
@staticmethod
57+
def _to_person(person_obj: str) -> Person:
58+
"""Parse poetry person string to a Person."""
59+
m = re.match(r"\s*([^<]+)<([^>]+)>", person_obj)
60+
names, mail = (
61+
list(map(lambda s: s.strip(), m.group(1).split())),
62+
m.group(2).strip(),
63+
)
64+
# NOTE: for our purposes, does not matter what are given or family names,
65+
# we only compare on full_name anyway.
66+
return Person(
67+
**{
68+
"given-names": " ".join(names[:-1]),
69+
"family-names": names[-1],
70+
"email": mail,
71+
}
72+
)
73+
74+
def sync(self, metadata: ProjectMetadata) -> None:
75+
"""Sync output file with other metadata files."""
76+
self.name = metadata.name
77+
78+
if metadata.version:
79+
self.version = metadata.version
80+
81+
self._sync_authors(metadata)

0 commit comments

Comments
 (0)