Coverage for src/somesy/fortran/writer.py: 84%

92 statements  

« prev     ^ index     » next       coverage.py v7.6.0, created at 2025-03-14 13:01 +0000

1"""Fortran writer.""" 

2 

3import logging 

4from pathlib import Path 

5from typing import List, Optional, Union 

6 

7import tomlkit 

8from rich.pretty import pretty_repr 

9 

10from somesy.core.models import Entity, Person, ProjectMetadata 

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

12 

13from .models import FortranConfig 

14 

15logger = logging.getLogger("somesy") 

16 

17 

18class Fortran(ProjectMetadataWriter): 

19 """Fortran config file handler parsed from fpm.toml.""" 

20 

21 def __init__( 

22 self, 

23 path: Path, 

24 pass_validation: Optional[bool] = False, 

25 ): 

26 """Fortran config file handler parsed from fpm.toml. 

27 

28 See [somesy.core.writer.ProjectMetadataWriter.__init__][]. 

29 """ 

30 mappings: FieldKeyMapping = { 

31 "authors": ["author"], 

32 "maintainers": ["maintainer"], 

33 "documentation": IgnoreKey(), 

34 } 

35 super().__init__( 

36 path, 

37 create_if_not_exists=False, 

38 direct_mappings=mappings, 

39 pass_validation=pass_validation, 

40 ) 

41 

42 @property 

43 def authors(self): 

44 """Return the only author of the fpm.toml file as list.""" 

45 authors = [] 

46 try: 

47 self._to_person(self._get_property(self._get_key("authors"))) 

48 authors = [self._get_property(self._get_key("authors"))] 

49 except ValueError: 

50 logger.warning("Cannot convert authors to Person object.") 

51 return authors 

52 

53 @authors.setter 

54 def authors(self, authors: List[Union[Person, Entity]]) -> None: 

55 """Set the authors of the project.""" 

56 self._set_property(self._get_key("authors"), self._from_person(authors[0])) 

57 

58 @property 

59 def maintainers(self): 

60 """Return the only author of the fpm.toml file as list.""" 

61 maintainers = self._get_property(self._get_key("maintainers")) 

62 if maintainers: 

63 return [self._get_property(self._get_key("maintainers"))] 

64 return [] 

65 

66 @maintainers.setter 

67 def maintainers(self, maintainers: List[Union[Person, Entity]]) -> None: 

68 """Set the maintainers of the project.""" 

69 maintainers = self._from_person(maintainers[0]) 

70 self._set_property(self._get_key("maintainers"), maintainers) 

71 

72 def _load(self) -> None: 

73 """Load fpm.toml file.""" 

74 with open(self.path) as f: 

75 self._data = tomlkit.load(f) 

76 

77 def _validate(self) -> None: 

78 """Validate poetry config using pydantic class. 

79 

80 In order to preserve toml comments and structure, tomlkit library is used. 

81 Pydantic class only used for validation. 

82 """ 

83 if self.pass_validation: 

84 return 

85 config = dict(self._get_property([])) 

86 logger.debug( 

87 f"Validating config using {FortranConfig.__name__}: {pretty_repr(config)}" 

88 ) 

89 FortranConfig(**config) 

90 

91 def save(self, path: Optional[Path] = None) -> None: 

92 """Save the fpm file.""" 

93 path = path or self.path 

94 if "description" in self._data: 

95 if "\n" in self._data["description"]: 

96 self._data["description"] = tomlkit.string( 

97 self._data["description"], multiline=True 

98 ) 

99 

100 # Handle arrays with proper formatting 

101 for key, value in self._data.items(): 

102 if isinstance(value, list): 

103 array = tomlkit.array() 

104 array.extend(value) 

105 array.multiline(True) 

106 # Ensure whitespace after commas in inline tables 

107 for item in array: 

108 if isinstance(item, tomlkit.items.InlineTable): 

109 # Rebuild the inline table with desired formatting 

110 formatted_item = tomlkit.inline_table() 

111 for k, v in item.value.items(): 

112 formatted_item[k] = v 

113 formatted_item.trivia.trail = " " # Add space after each comma 

114 array[array.index(item)] = formatted_item 

115 self._data[key] = array 

116 else: 

117 self._data[key] = value 

118 

119 with open(path, "w") as f: 

120 tomlkit.dump(self._data, f) 

121 

122 @staticmethod 

123 def _from_person(person: Union[Person, Entity]): 

124 """Convert project metadata person/entity object to poetry string for person format "full name <email>.""" 

125 return person.to_name_email_string() 

126 

127 @staticmethod 

128 def _to_person(person: str) -> Optional[Union[Person, Entity]]: 

129 """Convert from free string to person or entity object.""" 

130 try: 

131 return Person.from_name_email_string(person) 

132 except (ValueError, AttributeError): 

133 logger.info(f"Cannot convert {person} to Person object, trying Entity.") 

134 

135 try: 

136 return Entity.from_name_email_string(person) 

137 except (ValueError, AttributeError): 

138 logger.warning(f"Cannot convert {person} to Entity.") 

139 return None 

140 

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

142 """Sync output file with other metadata files.""" 

143 self.name = metadata.name 

144 self.description = metadata.description 

145 

146 if metadata.version: 

147 self.version = metadata.version 

148 

149 if metadata.keywords: 

150 self.keywords = metadata.keywords 

151 

152 self.authors = metadata.authors() 

153 maintainers = metadata.maintainers() 

154 

155 # set if not empty 

156 if maintainers: 

157 # only one maintainer is allowed 

158 self.maintainers = maintainers 

159 

160 self.license = metadata.license.value 

161 

162 self.homepage = str(metadata.homepage) if metadata.homepage else None