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

95 statements  

« prev     ^ index     » next       coverage.py v7.16.0, created at 2026-09-18 08:48 +0000

1"""Fortran writer.""" 

2 

3import logging 

4from pathlib import Path 

5 

6import tomlkit 

7from rich.pretty import pretty_repr 

8from tomlkit.items import InlineTable 

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: bool | None = 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[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[Person | Entity]) -> None: 

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

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

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

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: Path | None = None) -> None: 

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

93 path = path or self.path 

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

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

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

97 ) 

98 

99 # Handle arrays with proper formatting 

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

101 if isinstance(value, list): 

102 array = tomlkit.array() 

103 array.extend(value) 

104 array.multiline(True) 

105 # Ensure whitespace after commas in inline tables 

106 for item in array: 

107 if isinstance(item, InlineTable): 

108 # Rebuild the inline table with desired formatting 

109 formatted_item = tomlkit.inline_table() 

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

111 formatted_item[k] = v 

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

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

114 self._data[key] = array 

115 else: 

116 self._data[key] = value 

117 

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

119 tomlkit.dump(self._data, f) 

120 

121 @staticmethod 

122 def _from_person(person: Person | Entity): 

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

124 return person.to_name_email_string() 

125 

126 @staticmethod 

127 def _to_person(person_obj: str) -> Person | Entity | None: 

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

129 try: 

130 return Person.from_name_email_string(person_obj) 

131 except (ValueError, AttributeError): 

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

133 

134 try: 

135 return Entity.from_name_email_string(person_obj) 

136 except (ValueError, AttributeError): 

137 logger.warning(f"Cannot convert {person_obj} to Entity.") 

138 return None 

139 

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

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

142 if metadata.name is not None: 

143 self.name = metadata.name 

144 if metadata.description is not None: 

145 self.description = metadata.description 

146 

147 if metadata.version: 

148 self.version = metadata.version 

149 

150 if metadata.keywords: 

151 self.keywords = metadata.keywords 

152 

153 if metadata.authors(): 

154 self.authors = metadata.authors() 

155 maintainers = metadata.maintainers() 

156 

157 # set if not empty 

158 if maintainers: 

159 # only one maintainer is allowed 

160 self.maintainers = maintainers 

161 

162 if licenses := metadata.license: 

163 self.license = ( 

164 " OR ".join(license.value for license in licenses) 

165 if isinstance(licenses, list) 

166 else licenses.value 

167 ) 

168 

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