Coverage for src/somesy/rust/writer.py: 91%
76 statements
« prev ^ index » next coverage.py v7.3.2, created at 2024-04-30 09:42 +0000
« prev ^ index » next coverage.py v7.3.2, created at 2024-04-30 09:42 +0000
1"""Pyproject writers for setuptools and rust."""
2import logging
3from pathlib import Path
4from typing import Any, List, Optional, Union
6from rich.pretty import pretty_repr
7from tomlkit import dump, load, table
9from somesy.core.models import Person, ProjectMetadata
10from somesy.core.writer import FieldKeyMapping, IgnoreKey, ProjectMetadataWriter
12from .models import RustConfig, check_keyword
14logger = logging.getLogger("somesy")
17class Rust(ProjectMetadataWriter):
18 """Rust config file handler parsed from Cargo.toml."""
20 def __init__(self, path: Path):
21 """Rust config file handler parsed from Cargo.toml.
23 See [somesy.core.writer.ProjectMetadataWriter.__init__][].
24 """
25 self._section = ["package"]
26 mappings: FieldKeyMapping = {
27 "maintainers": IgnoreKey(),
28 }
29 super().__init__(path, create_if_not_exists=False, direct_mappings=mappings)
31 def _load(self) -> None:
32 """Load Cargo.toml file."""
33 with open(self.path) as f:
34 self._data = load(f)
36 def _validate(self) -> None:
37 """Validate rust config using pydantic class.
39 In order to preserve toml comments and structure, tomlkit library is used.
40 Pydantic class only used for validation.
41 """
42 config = dict(self._get_property([]))
43 logger.debug(
44 f"Validating config using {RustConfig.__name__}: {pretty_repr(config)}"
45 )
46 RustConfig(**config)
48 def save(self, path: Optional[Path] = None) -> None:
49 """Save the Cargo.toml file."""
50 path = path or self.path
51 with open(path, "w") as f:
52 dump(self._data, f)
54 def _get_property(
55 self, key: Union[str, List[str], IgnoreKey], *, remove: bool = False, **kwargs
56 ) -> Optional[Any]:
57 """Get a property from the Cargo.toml file."""
58 if isinstance(key, IgnoreKey):
59 return None
60 key_path = [key] if isinstance(key, str) else key
61 full_path = self._section + key_path
62 return super()._get_property(full_path, remove=remove, **kwargs)
64 def _set_property(self, key: Union[str, List[str], IgnoreKey], value: Any) -> None:
65 """Set a property in the Cargo.toml file."""
66 if isinstance(key, IgnoreKey):
67 return
68 key_path = [key] if isinstance(key, str) else key
70 if not value: # remove value and clean up the sub-dict
71 self._get_property(key_path, remove=True)
72 return
74 # get the tomlkit object of the section
75 dat = self._get_property([])
77 # dig down, create missing nested objects on the fly
78 curr = dat
79 for key in key_path[:-1]:
80 if key not in curr:
81 curr.add(key, table())
82 curr = curr[key]
83 curr[key_path[-1]] = value
85 @staticmethod
86 def _from_person(person: Person):
87 """Convert project metadata person object to rust string for person format "full name <email>."""
88 return person.to_name_email_string()
90 @staticmethod
91 def _to_person(person_obj: str) -> Person:
92 """Parse rust person string to a Person. It has format "full name <email>." but email is optional."""
93 try:
94 return Person.from_name_email_string(person_obj)
95 except (ValueError, AttributeError):
96 return None
98 @classmethod
99 def _parse_people(cls, people: Optional[List[Any]]) -> List[Person]:
100 """Return a list of Persons parsed from list of format-specific people representations. to_person can return None, so filter out None values."""
101 return list(filter(None, map(cls._to_person, people or [])))
103 @property
104 def keywords(self) -> Optional[List[str]]:
105 """Return the keywords of the project."""
106 return self._get_property(self._get_key("keywords"))
108 @keywords.setter
109 def keywords(self, keywords: List[str]) -> None:
110 """Set the keywords of the project."""
111 validated_keywords = []
112 for keyword in keywords:
113 try:
114 check_keyword(keyword)
115 validated_keywords.append(keyword)
116 except ValueError as e:
117 logger.debug(f"Invalid keyword {keyword}: {e}")
119 # keyword count should max 5, so delete the rest
120 if len(validated_keywords) > 5:
121 validated_keywords = validated_keywords[:5]
122 self._set_property(self._get_key("keywords"), validated_keywords)
124 def sync(self, metadata: ProjectMetadata) -> None:
125 """Sync the rust config with the project metadata."""
126 super().sync(metadata)
128 # if there is a license file, remove the license field
129 if self._get_key("license_file"):
130 self.license = None