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
« 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
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
17from somesy.core.models import LicenseEnum
18from somesy.core.types import HttpUrlStr
20EMailAddress = TypeAdapter(EmailStr)
23class PoetryConfig(BaseModel):
24 """Poetry configuration model."""
26 model_config = dict(use_enum_values=True)
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
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
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
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")
105class ContentTypeEnum(Enum):
106 """Content type enum for setuptools field file."""
108 plain = "text/plain"
109 rst = "text/x-rst"
110 markdown = "text/markdown"
113class File(BaseModel):
114 """File model for setuptools."""
116 file: Path
117 content_type: Optional[ContentTypeEnum] = Field(alias="content-type")
120class License(BaseModel):
121 """License model for setuptools."""
123 model_config = dict(validate_assignment=True)
125 file: Optional[Path] = None
126 text: Optional[LicenseEnum] = None
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
137class STPerson(BaseModel):
138 """Person model for setuptools."""
140 name: Annotated[str, Field(min_length=1)]
141 email: Annotated[str, Field(min_length=1)]
144class URLs(BaseModel):
145 """URLs model for setuptools."""
147 homepage: Optional[HttpUrlStr] = None
148 repository: Optional[HttpUrlStr] = None
149 documentation: Optional[HttpUrlStr] = None
150 changelog: Optional[HttpUrlStr] = None
153class SetuptoolsConfig(BaseModel):
154 """Setuptools input model. Required fields are name, version, description, and requires_python."""
156 model_config = dict(use_enum_values=True)
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
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
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")
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