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
« prev ^ index » next coverage.py v7.6.0, created at 2024-07-29 07:50 +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 PoetryConfig(BaseModel):
28 """Poetry configuration model."""
30 model_config = dict(use_enum_values=True)
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
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
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
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")
121class ContentTypeEnum(Enum):
122 """Content type enum for setuptools field file."""
124 plain = "text/plain"
125 rst = "text/x-rst"
126 markdown = "text/markdown"
129class File(BaseModel):
130 """File model for setuptools."""
132 file: Path
133 content_type: Optional[ContentTypeEnum] = Field(alias="content-type")
136class License(BaseModel):
137 """License model for setuptools."""
139 model_config = dict(validate_assignment=True)
141 file: Optional[Path] = None
142 text: Optional[LicenseEnum] = None
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
153class STPerson(BaseModel):
154 """Person model for setuptools."""
156 name: Annotated[str, Field(min_length=1)]
157 email: Annotated[str, Field(min_length=1)]
160class URLs(BaseModel):
161 """URLs model for setuptools."""
163 homepage: Optional[HttpUrlStr] = None
164 repository: Optional[HttpUrlStr] = None
165 documentation: Optional[HttpUrlStr] = None
166 changelog: Optional[HttpUrlStr] = None
169class SetuptoolsConfig(BaseModel):
170 """Setuptools input model. Required fields are name, version, description, and requires_python."""
172 model_config = dict(use_enum_values=True)
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
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
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")
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