Coverage for src/somesy/rust/writer.py: 91%

76 statements  

« prev     ^ index     » next       coverage.py v7.6.0, created at 2024-07-29 07:42 +0000

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

2 

3import logging 

4from pathlib import Path 

5from typing import Any, List, Optional, Union 

6 

7from rich.pretty import pretty_repr 

8from tomlkit import dump, load, table 

9 

10from somesy.core.models import Person, ProjectMetadata 

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

12 

13from .models import RustConfig, check_keyword 

14 

15logger = logging.getLogger("somesy") 

16 

17 

18class Rust(ProjectMetadataWriter): 

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

20 

21 def __init__(self, path: Path): 

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

23 

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) 

31 

32 def _load(self) -> None: 

33 """Load Cargo.toml file.""" 

34 with open(self.path) as f: 

35 self._data = load(f) 

36 

37 def _validate(self) -> None: 

38 """Validate rust config using pydantic class. 

39 

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) 

48 

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) 

54 

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) 

64 

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 

70 

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

72 self._get_property(key_path, remove=True) 

73 return 

74 

75 # get the tomlkit object of the section 

76 dat = self._get_property([]) 

77 

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 

85 

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

90 

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 

98 

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

103 

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

108 

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

119 

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) 

124 

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

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

127 super().sync(metadata) 

128 

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

130 if self._get_key("license_file"): 

131 self.license = None