Coverage for src/somesy/pyproject/models.py: 79%
136 statements
« prev ^ index » next coverage.py v7.6.0, created at 2025-03-10 14:56 +0000
« prev ^ index » next coverage.py v7.6.0, created at 2025-03-10 14:56 +0000
1"""Pyproject models."""
3from enum import Enum
4from logging import getLogger
5from pathlib import Path
6from typing import Dict, List, Optional, Set, Union
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
20from somesy.core.models import LicenseEnum
21from somesy.core.types import HttpUrlStr
23EMailAddress = TypeAdapter(EmailStr)
24logger = getLogger("somesy")
27class STPerson(BaseModel):
28 """Person model for setuptools."""
30 name: Annotated[str, Field(min_length=1)]
31 email: Annotated[Optional[str], Field(min_length=1)] = None
34class License(BaseModel):
35 """License model for setuptools."""
37 model_config = dict(validate_assignment=True)
39 file: Optional[Path] = None
40 text: Optional[LicenseEnum] = None
42 @model_validator(mode="before")
43 @classmethod
44 def validate_xor(cls, values):
45 """Validate that only one of file or text is set."""
46 # check if this has just str or list of str
47 if isinstance(values, str):
48 if values in LicenseEnum.__members__:
49 return {"text": values}
50 else:
51 raise ValueError("Invalid license.")
52 if isinstance(values, list):
53 # check if all elements are valid string for LicenseEnum
54 for v in values:
55 if not isinstance(v, str):
56 raise ValueError("All elements must be strings.")
57 if v not in LicenseEnum.__members__:
58 raise ValueError("Invalid license.")
59 return values
60 if sum([bool(v) for v in values.values()]) != 1:
61 raise ValueError("Either file or text must be set.")
62 return values
65class PoetryConfig(BaseModel):
66 """Poetry configuration model."""
68 model_config = dict(use_enum_values=True)
70 name: Annotated[
71 str,
72 Field(pattern=r"^[A-Za-z0-9]+([_-][A-Za-z0-9]+)*$", description="Package name"),
73 ]
74 version: Annotated[
75 str,
76 Field(
77 pattern=r"^\d+(\.\d+)*((a|b|rc)\d+)?(post\d+)?(dev\d+)?$",
78 description="Package version",
79 ),
80 ]
81 description: Annotated[str, Field(description="Package description")]
82 license: Annotated[
83 Optional[Union[LicenseEnum, List[LicenseEnum], License]],
84 Field(description="An SPDX license identifier."),
85 ]
87 # v1 has str, v2 has STPerson
88 authors: Annotated[List[Union[str, STPerson]], Field(description="Package authors")]
89 maintainers: Annotated[
90 Optional[List[Union[str, STPerson]]], Field(description="Package maintainers")
91 ] = None
93 readme: Annotated[
94 Optional[Union[Path, List[Path]]], Field(description="Package readme file(s)")
95 ] = None
96 homepage: Annotated[Optional[HttpUrlStr], Field(description="Package homepage")] = (
97 None
98 )
99 repository: Annotated[
100 Optional[HttpUrlStr], Field(description="Package repository")
101 ] = None
102 documentation: Annotated[
103 Optional[HttpUrlStr], Field(description="Package documentation page")
104 ] = None
105 keywords: Annotated[
106 Optional[Set[str]], Field(description="Keywords that describe the package")
107 ] = None
108 classifiers: Annotated[
109 Optional[List[str]], Field(description="pypi classifiers")
110 ] = None
111 urls: Annotated[
112 Optional[Dict[str, HttpUrlStr]], Field(description="Package URLs")
113 ] = None
115 @field_validator("version")
116 @classmethod
117 def validate_version(cls, v):
118 """Validate version using PEP 440."""
119 try:
120 _ = parse_version(v)
121 except ValueError as err:
122 raise ValueError("Invalid version") from err
123 return v
125 @field_validator("authors", "maintainers")
126 @classmethod
127 def validate_email_format(cls, v):
128 """Validate person format, omit person that is not in correct format, don't raise an error."""
129 if v is None:
130 return []
131 validated = []
132 for author in v:
133 try:
134 if isinstance(author, STPerson) and author.email:
135 if not EMailAddress.validate_python(author.email):
136 logger.warning(
137 f"Invalid email format for author/maintainer {author}, omitting."
138 )
139 else:
140 validated.append(author)
141 continue
143 if " " in author and EMailAddress.validate_python(
144 author.split(" ")[-1][1:-1]
145 ):
146 validated.append(author)
147 else:
148 logger.warning(
149 f"Invalid email format for author/maintainer {author}, omitting."
150 )
151 except ValidationError:
152 logger.warning(
153 f"Invalid email format for author/maintainer {author}, omitting."
154 )
155 return validated
157 @field_validator("readme")
158 @classmethod
159 def validate_readme(cls, v):
160 """Validate readme file(s) by checking whether files exist."""
161 if isinstance(v, list):
162 if any(not e.is_file() for e in v):
163 logger.warning("Some readme file(s) do not exist")
164 else:
165 if not v.is_file():
166 logger.warning("Readme file does not exist")
169class ContentTypeEnum(Enum):
170 """Content type enum for setuptools field file."""
172 plain = "text/plain"
173 rst = "text/x-rst"
174 markdown = "text/markdown"
177class File(BaseModel):
178 """File model for setuptools."""
180 file: Path
181 content_type: Optional[ContentTypeEnum] = Field(alias="content-type")
184class URLs(BaseModel):
185 """URLs model for setuptools."""
187 homepage: Optional[HttpUrlStr] = None
188 repository: Optional[HttpUrlStr] = None
189 documentation: Optional[HttpUrlStr] = None
190 changelog: Optional[HttpUrlStr] = None
193class SetuptoolsConfig(BaseModel):
194 """Setuptools input model. Required fields are name, version, description, and requires_python."""
196 model_config = dict(use_enum_values=True)
198 name: Annotated[str, Field(pattern=r"^[A-Za-z0-9]+([_-][A-Za-z0-9]+)*$")]
199 version: Annotated[
200 str, Field(pattern=r"^\d+(\.\d+)*((a|b|rc)\d+)?(post\d+)?(dev\d+)?$")
201 ]
202 description: str
203 readme: Optional[Union[Path, List[Path], File]] = None
204 license: Optional[License] = Field(None, description="An SPDX license identifier.")
205 authors: Optional[List[STPerson]] = None
206 maintainers: Optional[List[STPerson]] = None
207 keywords: Optional[Set[str]] = None
208 classifiers: Optional[List[str]] = None
209 urls: Optional[URLs] = None
211 @field_validator("version")
212 @classmethod
213 def validate_version(cls, v):
214 """Validate version using PEP 440."""
215 try:
216 _ = parse_version(v)
217 except ValueError as err:
218 raise ValueError("Invalid version") from err
219 return v
221 @field_validator("readme")
222 @classmethod
223 def validate_readme(cls, v):
224 """Validate readme file(s) by checking whether files exist."""
225 if isinstance(v, list):
226 if any(not e.is_file() for e in v):
227 raise ValueError("Some file(s) do not exist")
228 elif type(v) is File:
229 if not Path(v.file).is_file():
230 raise ValueError("File does not exist")
231 else:
232 if not v.is_file():
233 raise ValueError("File does not exist")
235 @field_validator("authors", "maintainers")
236 @classmethod
237 def validate_email_format(cls, v):
238 """Validate email format."""
239 for person in v:
240 if person.email:
241 if not EMailAddress.validate_python(person.email):
242 raise ValueError("Invalid email format")
243 return v