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

62 statements  

« prev     ^ index     » next       coverage.py v7.6.0, created at 2024-07-29 07:42 +0000

1"""Pyproject models.""" 

2 

3import re 

4from pathlib import Path 

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

6 

7from packaging.version import parse as parse_version 

8from pydantic import BaseModel, Field, field_validator, model_validator 

9from typing_extensions import Annotated 

10 

11from somesy.core.types import HttpUrlStr 

12 

13 

14class RustConfig(BaseModel): 

15 """Rust configuration model.""" 

16 

17 model_config = dict(use_enum_values=True) 

18 

19 name: Annotated[ 

20 str, 

21 Field( 

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

23 max_length=64, 

24 description="Package name", 

25 ), 

26 ] 

27 version: Annotated[ 

28 str, 

29 Field( 

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

31 description="Package version", 

32 ), 

33 ] 

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

35 None 

36 ) 

37 license: Annotated[ 

38 Optional[str], 

39 Field( 

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

41 ), 

42 ] = None 

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

44 maintainers: Annotated[ 

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

46 ] = None 

47 readme: Annotated[ 

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

49 ] = None 

50 license_file: Annotated[ 

51 Optional[Path], Field(description="Package license file") 

52 ] = None 

53 homepage: Annotated[Optional[HttpUrlStr], Field(description="Package homepage")] = ( 

54 None 

55 ) 

56 repository: Annotated[ 

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

58 ] = None 

59 documentation: Annotated[ 

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

61 ] = None 

62 keywords: Annotated[ 

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

64 ] = None 

65 classifiers: Annotated[ 

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

67 ] = None 

68 urls: Annotated[ 

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

70 ] = None 

71 

72 @model_validator(mode="before") 

73 @classmethod 

74 def license_or_file(cls, values): 

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

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

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

78 return values 

79 

80 @field_validator("version") 

81 @classmethod 

82 def validate_version(cls, v): 

83 """Validate version using PEP 440.""" 

84 try: 

85 _ = parse_version(v) 

86 except ValueError as err: 

87 raise ValueError("Invalid version") from err 

88 return v 

89 

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

91 @classmethod 

92 def validate_readme(cls, v): 

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

94 if isinstance(v, list): 

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

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

97 else: 

98 if not v.is_file(): 

99 raise ValueError("File does not exist") 

100 

101 @field_validator("keywords") 

102 @classmethod 

103 def check_keywords_field(cls, v): 

104 """Check the keywords field.""" 

105 if v is None: 

106 return v 

107 

108 # Check if number of keywords is at most 5 

109 if v is not None and len(v) > 5: 

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

111 

112 for keyword in v: 

113 check_keyword(keyword) 

114 

115 return v 

116 

117 

118def check_keyword(keyword: str): 

119 """Check if keyword is valid.""" 

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

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

122 raise ValueError( 

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

124 ) 

125 

126 # Check if keyword starts with an alphanumeric character 

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

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

129 

130 # Check if keyword contains only allowed characters 

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

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