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