Coverage for src/somesy/pyproject/models.py: 82%

120 statements  

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

1"""Pyproject models.""" 

2 

3from enum import Enum 

4from logging import getLogger 

5from pathlib import Path 

6from typing import Dict, List, Optional, Set, Union 

7 

8from packaging.version import parse as parse_version 

9from pydantic import ( 

10 BaseModel, 

11 EmailStr, 

12 Field, 

13 TypeAdapter, 

14 ValidationError, 

15 field_validator, 

16 model_validator, 

17) 

18from typing_extensions import Annotated 

19 

20from somesy.core.models import LicenseEnum 

21from somesy.core.types import HttpUrlStr 

22 

23EMailAddress = TypeAdapter(EmailStr) 

24logger = getLogger("somesy") 

25 

26 

27class PoetryConfig(BaseModel): 

28 """Poetry configuration model.""" 

29 

30 model_config = dict(use_enum_values=True) 

31 

32 name: Annotated[ 

33 str, 

34 Field(pattern=r"^[A-Za-z0-9]+([_-][A-Za-z0-9]+)*$", description="Package name"), 

35 ] 

36 version: Annotated[ 

37 str, 

38 Field( 

39 pattern=r"^\d+(\.\d+)*((a|b|rc)\d+)?(post\d+)?(dev\d+)?$", 

40 description="Package version", 

41 ), 

42 ] 

43 description: Annotated[str, Field(description="Package description")] 

44 license: Annotated[ 

45 Optional[Union[LicenseEnum, List[LicenseEnum]]], 

46 Field(description="An SPDX license identifier."), 

47 ] 

48 authors: Annotated[Set[str], Field(description="Package authors")] 

49 maintainers: Annotated[ 

50 Optional[Set[str]], Field(description="Package maintainers") 

51 ] = None 

52 readme: Annotated[ 

53 Optional[Union[Path, List[Path]]], Field(description="Package readme file(s)") 

54 ] = None 

55 homepage: Annotated[Optional[HttpUrlStr], Field(description="Package homepage")] = ( 

56 None 

57 ) 

58 repository: Annotated[ 

59 Optional[HttpUrlStr], Field(description="Package repository") 

60 ] = None 

61 documentation: Annotated[ 

62 Optional[HttpUrlStr], Field(description="Package documentation page") 

63 ] = None 

64 keywords: Annotated[ 

65 Optional[Set[str]], Field(description="Keywords that describe the package") 

66 ] = None 

67 classifiers: Annotated[ 

68 Optional[List[str]], Field(description="pypi classifiers") 

69 ] = None 

70 urls: Annotated[ 

71 Optional[Dict[str, HttpUrlStr]], Field(description="Package URLs") 

72 ] = None 

73 

74 @field_validator("version") 

75 @classmethod 

76 def validate_version(cls, v): 

77 """Validate version using PEP 440.""" 

78 try: 

79 _ = parse_version(v) 

80 except ValueError as err: 

81 raise ValueError("Invalid version") from err 

82 return v 

83 

84 @field_validator("authors", "maintainers") 

85 @classmethod 

86 def validate_email_format(cls, v): 

87 """Validate person format, omit person that is not in correct format, don't raise an error.""" 

88 if v is None: 

89 return [] 

90 validated = [] 

91 for author in v: 

92 try: 

93 if not ( 

94 not isinstance(author, str) 

95 or " " not in author 

96 or not EMailAddress.validate_python(author.split(" ")[-1][1:-1]) 

97 ): 

98 validated.append(author) 

99 else: 

100 logger.warning( 

101 f"Invalid email format for author/maintainer {author}, omitting." 

102 ) 

103 except ValidationError: 

104 logger.warning( 

105 f"Invalid email format for author/maintainer {author}, omitting." 

106 ) 

107 return validated 

108 

109 @field_validator("readme") 

110 @classmethod 

111 def validate_readme(cls, v): 

112 """Validate readme file(s) by checking whether files exist.""" 

113 if isinstance(v, list): 

114 if any(not e.is_file() for e in v): 

115 logger.warning("Some readme file(s) do not exist") 

116 else: 

117 if not v.is_file(): 

118 logger.warning("Readme file does not exist") 

119 

120 

121class ContentTypeEnum(Enum): 

122 """Content type enum for setuptools field file.""" 

123 

124 plain = "text/plain" 

125 rst = "text/x-rst" 

126 markdown = "text/markdown" 

127 

128 

129class File(BaseModel): 

130 """File model for setuptools.""" 

131 

132 file: Path 

133 content_type: Optional[ContentTypeEnum] = Field(alias="content-type") 

134 

135 

136class License(BaseModel): 

137 """License model for setuptools.""" 

138 

139 model_config = dict(validate_assignment=True) 

140 

141 file: Optional[Path] = None 

142 text: Optional[LicenseEnum] = None 

143 

144 @model_validator(mode="before") 

145 @classmethod 

146 def validate_xor(cls, values): 

147 """Validate that only one of file or text is set.""" 

148 if sum([bool(v) for v in values.values()]) != 1: 

149 raise ValueError("Either file or text must be set.") 

150 return values 

151 

152 

153class STPerson(BaseModel): 

154 """Person model for setuptools.""" 

155 

156 name: Annotated[str, Field(min_length=1)] 

157 email: Annotated[str, Field(min_length=1)] 

158 

159 

160class URLs(BaseModel): 

161 """URLs model for setuptools.""" 

162 

163 homepage: Optional[HttpUrlStr] = None 

164 repository: Optional[HttpUrlStr] = None 

165 documentation: Optional[HttpUrlStr] = None 

166 changelog: Optional[HttpUrlStr] = None 

167 

168 

169class SetuptoolsConfig(BaseModel): 

170 """Setuptools input model. Required fields are name, version, description, and requires_python.""" 

171 

172 model_config = dict(use_enum_values=True) 

173 

174 name: Annotated[str, Field(pattern=r"^[A-Za-z0-9]+([_-][A-Za-z0-9]+)*$")] 

175 version: Annotated[ 

176 str, Field(pattern=r"^\d+(\.\d+)*((a|b|rc)\d+)?(post\d+)?(dev\d+)?$") 

177 ] 

178 description: str 

179 readme: Optional[Union[Path, List[Path], File]] = None 

180 license: Optional[License] = Field(None, description="An SPDX license identifier.") 

181 authors: Optional[List[STPerson]] = None 

182 maintainers: Optional[List[STPerson]] = None 

183 keywords: Optional[Set[str]] = None 

184 classifiers: Optional[List[str]] = None 

185 urls: Optional[URLs] = None 

186 

187 @field_validator("version") 

188 @classmethod 

189 def validate_version(cls, v): 

190 """Validate version using PEP 440.""" 

191 try: 

192 _ = parse_version(v) 

193 except ValueError as err: 

194 raise ValueError("Invalid version") from err 

195 return v 

196 

197 @field_validator("readme") 

198 @classmethod 

199 def validate_readme(cls, v): 

200 """Validate readme file(s) by checking whether files exist.""" 

201 if isinstance(v, list): 

202 if any(not e.is_file() for e in v): 

203 raise ValueError("Some file(s) do not exist") 

204 elif type(v) is File: 

205 if not Path(v.file).is_file(): 

206 raise ValueError("File does not exist") 

207 else: 

208 if not v.is_file(): 

209 raise ValueError("File does not exist") 

210 

211 @field_validator("authors", "maintainers") 

212 @classmethod 

213 def validate_email_format(cls, v): 

214 """Validate email format.""" 

215 for person in v: 

216 if person.email: 

217 if not EMailAddress.validate_python(person.email): 

218 raise ValueError("Invalid email format") 

219 return v