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

111 statements  

« prev     ^ index     » next       coverage.py v7.3.2, created at 2024-04-30 09:42 +0000

1"""Pyproject models.""" 

2from enum import Enum 

3from pathlib import Path 

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

5 

6from packaging.version import parse as parse_version 

7from pydantic import ( 

8 BaseModel, 

9 EmailStr, 

10 Field, 

11 TypeAdapter, 

12 field_validator, 

13 model_validator, 

14) 

15from typing_extensions import Annotated 

16 

17from somesy.core.models import LicenseEnum 

18from somesy.core.types import HttpUrlStr 

19 

20EMailAddress = TypeAdapter(EmailStr) 

21 

22 

23class PoetryConfig(BaseModel): 

24 """Poetry configuration model.""" 

25 

26 model_config = dict(use_enum_values=True) 

27 

28 name: Annotated[ 

29 str, 

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

31 ] 

32 version: Annotated[ 

33 str, 

34 Field( 

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

36 description="Package version", 

37 ), 

38 ] 

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

40 license: Annotated[ 

41 Optional[Union[LicenseEnum, List[LicenseEnum]]], 

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

43 ] 

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

45 maintainers: Annotated[ 

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

47 ] = None 

48 readme: Annotated[ 

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

50 ] = None 

51 homepage: Annotated[ 

52 Optional[HttpUrlStr], Field(description="Package homepage") 

53 ] = None 

54 repository: Annotated[ 

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

56 ] = None 

57 documentation: Annotated[ 

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

59 ] = None 

60 keywords: Annotated[ 

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

62 ] = None 

63 classifiers: Annotated[ 

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

65 ] = None 

66 urls: Annotated[ 

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

68 ] = None 

69 

70 @field_validator("version") 

71 @classmethod 

72 def validate_version(cls, v): 

73 """Validate version using PEP 440.""" 

74 try: 

75 _ = parse_version(v) 

76 except ValueError as err: 

77 raise ValueError("Invalid version") from err 

78 return v 

79 

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

81 @classmethod 

82 def validate_email_format(cls, v): 

83 """Validate email format.""" 

84 for author in v: 

85 if ( 

86 not isinstance(author, str) 

87 or " " not in author 

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

89 ): 

90 raise ValueError("Invalid email format") 

91 return v 

92 

93 @field_validator("readme") 

94 @classmethod 

95 def validate_readme(cls, v): 

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

97 if isinstance(v, list): 

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

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

100 else: 

101 if not v.is_file(): 

102 raise ValueError("File does not exist") 

103 

104 

105class ContentTypeEnum(Enum): 

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

107 

108 plain = "text/plain" 

109 rst = "text/x-rst" 

110 markdown = "text/markdown" 

111 

112 

113class File(BaseModel): 

114 """File model for setuptools.""" 

115 

116 file: Path 

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

118 

119 

120class License(BaseModel): 

121 """License model for setuptools.""" 

122 

123 model_config = dict(validate_assignment=True) 

124 

125 file: Optional[Path] = None 

126 text: Optional[LicenseEnum] = None 

127 

128 @model_validator(mode="before") 

129 @classmethod 

130 def validate_xor(cls, values): 

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

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

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

134 return values 

135 

136 

137class STPerson(BaseModel): 

138 """Person model for setuptools.""" 

139 

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

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

142 

143 

144class URLs(BaseModel): 

145 """URLs model for setuptools.""" 

146 

147 homepage: Optional[HttpUrlStr] = None 

148 repository: Optional[HttpUrlStr] = None 

149 documentation: Optional[HttpUrlStr] = None 

150 changelog: Optional[HttpUrlStr] = None 

151 

152 

153class SetuptoolsConfig(BaseModel): 

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

155 

156 model_config = dict(use_enum_values=True) 

157 

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

159 version: Annotated[ 

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

161 ] 

162 description: str 

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

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

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

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

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

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

169 urls: Optional[URLs] = None 

170 

171 @field_validator("version") 

172 @classmethod 

173 def validate_version(cls, v): 

174 """Validate version using PEP 440.""" 

175 try: 

176 _ = parse_version(v) 

177 except ValueError as err: 

178 raise ValueError("Invalid version") from err 

179 return v 

180 

181 @field_validator("readme") 

182 @classmethod 

183 def validate_readme(cls, v): 

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

185 if isinstance(v, list): 

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

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

188 elif type(v) is File: 

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

190 raise ValueError("File does not exist") 

191 else: 

192 if not v.is_file(): 

193 raise ValueError("File does not exist") 

194 

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

196 @classmethod 

197 def validate_email_format(cls, v): 

198 """Validate email format.""" 

199 for person in v: 

200 if person.email: 

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

202 raise ValueError("Invalid email format") 

203 return v