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

62 statements  

« prev     ^ index     » next       coverage.py v7.3.2, created at 2024-04-30 09:42 +0000

1"""Pyproject models.""" 

2import re 

3from pathlib import Path 

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

5 

6from packaging.version import parse as parse_version 

7from pydantic import BaseModel, Field, field_validator, model_validator 

8from typing_extensions import Annotated 

9 

10from somesy.core.types import HttpUrlStr 

11 

12 

13class RustConfig(BaseModel): 

14 """Rust configuration model.""" 

15 

16 model_config = dict(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[ 

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

35 ] = None 

36 license: Annotated[ 

37 Optional[str], 

38 Field( 

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

40 ), 

41 ] = None 

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

43 maintainers: Annotated[ 

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

45 ] = None 

46 readme: Annotated[ 

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

48 ] = None 

49 license_file: Annotated[ 

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

51 ] = None 

52 homepage: Annotated[ 

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

54 ] = None 

55 repository: Annotated[ 

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

57 ] = None 

58 documentation: Annotated[ 

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

60 ] = None 

61 keywords: Annotated[ 

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

63 ] = None 

64 classifiers: Annotated[ 

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

66 ] = None 

67 urls: Annotated[ 

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

69 ] = None 

70 

71 @model_validator(mode="before") 

72 @classmethod 

73 def license_or_file(cls, values): 

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

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

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

77 return values 

78 

79 @field_validator("version") 

80 @classmethod 

81 def validate_version(cls, v): 

82 """Validate version using PEP 440.""" 

83 try: 

84 _ = parse_version(v) 

85 except ValueError as err: 

86 raise ValueError("Invalid version") from err 

87 return v 

88 

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

90 @classmethod 

91 def validate_readme(cls, v): 

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

93 if isinstance(v, list): 

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

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

96 else: 

97 if not v.is_file(): 

98 raise ValueError("File does not exist") 

99 

100 @field_validator("keywords") 

101 @classmethod 

102 def check_keywords_field(cls, v): 

103 """Check the keywords field.""" 

104 if v is None: 

105 return v 

106 

107 # Check if number of keywords is at most 5 

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

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

110 

111 for keyword in v: 

112 check_keyword(keyword) 

113 

114 return v 

115 

116 

117def check_keyword(keyword: str): 

118 """Check if keyword is valid.""" 

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

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

121 raise ValueError( 

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

123 ) 

124 

125 # Check if keyword starts with an alphanumeric character 

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

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

128 

129 # Check if keyword contains only allowed characters 

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

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