Coverage for src/somesy/rust/models.py: 85%

61 statements  

« prev     ^ index     » next       coverage.py v7.16.0, created at 2026-09-04 11:35 +0000

1"""Pyproject models.""" 

2 

3import re 

4from pathlib import Path 

5from typing import Annotated 

6 

7from packaging.version import parse as parse_version 

8from pydantic import BaseModel, Field, field_validator, model_validator 

9 

10from somesy.core.types import HttpUrlStr 

11 

12 

13class RustConfig(BaseModel): 

14 """Rust configuration model.""" 

15 

16 model_config = {"use_enum_values": True} 

17 

18 name: Annotated[ 

19 str, 

20 Field( 

21 pattern=r"^[A-Za-z0-9]+([_-][A-Za-z0-9]+)*$", 

22 max_length=64, 

23 description="Package name", 

24 ), 

25 ] 

26 version: Annotated[ 

27 str, 

28 Field( 

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

30 description="Package version", 

31 ), 

32 ] 

33 description: Annotated[str | None, Field(description="Package description")] = None 

34 license: Annotated[ 

35 str | None, 

36 Field( 

37 description="A combination SPDX license identifiers with AND, OR and so on." 

38 ), 

39 ] = None 

40 authors: Annotated[set[str], Field(description="Package authors")] 

41 maintainers: Annotated[ 

42 set[str] | None, Field(description="Package maintainers") 

43 ] = None 

44 readme: Annotated[ 

45 Path | list[Path] | None, Field(description="Package readme file(s)") 

46 ] = None 

47 license_file: Annotated[Path | None, Field(description="Package license file")] = ( 

48 None 

49 ) 

50 homepage: Annotated[HttpUrlStr | None, Field(description="Package homepage")] = None 

51 repository: Annotated[ 

52 HttpUrlStr | None, Field(description="Package repository") 

53 ] = None 

54 documentation: Annotated[ 

55 HttpUrlStr | None, Field(description="Package documentation page") 

56 ] = None 

57 keywords: Annotated[ 

58 set[str] | None, Field(description="Keywords that describe the package") 

59 ] = None 

60 classifiers: Annotated[list[str] | None, Field(description="pypi classifiers")] = ( 

61 None 

62 ) 

63 urls: Annotated[dict[str, HttpUrlStr] | None, Field(description="Package URLs")] = ( 

64 None 

65 ) 

66 

67 @model_validator(mode="before") 

68 @classmethod 

69 def license_or_file(cls, values): 

70 """License and license file are mutually exclusive.""" 

71 if "license" in values and "license_file" in values: 

72 raise ValueError("license and license_file are mutually exclusive") 

73 return values 

74 

75 @field_validator("version") 

76 @classmethod 

77 def validate_version(cls, v): 

78 """Validate version using PEP 440.""" 

79 try: 

80 _ = parse_version(v) 

81 except ValueError as err: 

82 raise ValueError("Invalid version") from err 

83 return v 

84 

85 @field_validator("readme", "license_file") 

86 @classmethod 

87 def validate_readme(cls, v): 

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

89 if isinstance(v, list): 

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

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

92 else: 

93 if not v.is_file(): 

94 raise ValueError("File does not exist") 

95 

96 @field_validator("keywords") 

97 @classmethod 

98 def check_keywords_field(cls, v): 

99 """Check the keywords field.""" 

100 if v is None: 

101 return v 

102 

103 # Check if number of keywords is at most 5 

104 if len(v) > 5: 

105 raise ValueError("A maximum of 5 keywords is allowed") 

106 

107 for keyword in v or []: 

108 check_keyword(keyword) 

109 

110 return v 

111 

112 

113def check_keyword(keyword: str): 

114 """Check if keyword is valid.""" 

115 # Check if keyword is ASCII and has at most 20 characters 

116 if not keyword.isascii() or len(keyword) > 20: 

117 raise ValueError( 

118 "Each keyword must be ASCII text and have at most 20 characters" 

119 ) 

120 

121 # Check if keyword starts with an alphanumeric character 

122 if not re.match(r"^[a-zA-Z0-9]", keyword): 

123 raise ValueError("Each keyword must start with an alphanumeric character") 

124 

125 # Check if keyword contains only allowed characters 

126 if not re.match(r"^[a-zA-Z0-9_\-+]+$", keyword): 

127 raise ValueError("Keywords can only contain letters, numbers, _, -, or +")