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

1"""Pyproject writers for setuptools and rust.""" 

2import logging 

3from pathlib import Path 

4from typing import Any, List, Optional, Union 

5 

6from rich.pretty import pretty_repr 

7from tomlkit import dump, load, table 

8 

9from somesy.core.models import Person, ProjectMetadata 

10from somesy.core.writer import FieldKeyMapping, IgnoreKey, ProjectMetadataWriter 

11 

12from .models import RustConfig, check_keyword 

13 

14logger = logging.getLogger("somesy") 

15 

16 

17class Rust(ProjectMetadataWriter): 

18 """Rust config file handler parsed from Cargo.toml.""" 

19 

20 def __init__(self, path: Path): 

21 """Rust config file handler parsed from Cargo.toml. 

22 

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) 

30 

31 def _load(self) -> None: 

32 """Load Cargo.toml file.""" 

33 with open(self.path) as f: 

34 self._data = load(f) 

35 

36 def _validate(self) -> None: 

37 """Validate rust config using pydantic class. 

38 

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) 

47 

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) 

53 

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) 

63 

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 

69 

70 if not value: # remove value and clean up the sub-dict 

71 self._get_property(key_path, remove=True) 

72 return 

73 

74 # get the tomlkit object of the section 

75 dat = self._get_property([]) 

76 

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 

84 

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() 

89 

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 

97 

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 []))) 

102 

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")) 

107 

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}") 

118 

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) 

123 

124 def sync(self, metadata: ProjectMetadata) -> None: 

125 """Sync the rust config with the project metadata.""" 

126 super().sync(metadata) 

127 

128 # if there is a license file, remove the license field 

129 if self._get_key("license_file"): 

130 self.license = None