Coverage for enderchest/instance.py: 100%

49 statements  

« prev     ^ index     » next       coverage.py v7.5.0, created at 2024-05-04 01:41 +0000

1"""Specification of a Minecraft instance""" 

2 

3import re 

4from configparser import SectionProxy 

5from pathlib import Path 

6from typing import NamedTuple 

7 

8from . import config as cfg 

9 

10 

11class InstanceSpec(NamedTuple): 

12 """Specification of a Minecraft instance 

13 

14 Parameters 

15 ---------- 

16 name : str 

17 The "display name" for the instance 

18 root : Path 

19 The path to its ".minecraft" folder 

20 minecraft_versions : list-like of str 

21 The minecraft versions of this instance. This is typically a 1-tuple, 

22 but some loaders (such as the official one) will just comingle all 

23 your assets together across all profiles 

24 modloader : str 

25 The (display) name of the modloader (vanilla corresponds to "") 

26 tags : list-like of str 

27 The tags assigned to this instance, including both the ones assigned 

28 in the launcher (groups) and the ones assigned by hand. 

29 """ 

30 

31 name: str 

32 root: Path 

33 minecraft_versions: tuple[str, ...] 

34 modloader: str 

35 groups_: tuple[str, ...] 

36 tags_: tuple[str, ...] 

37 

38 @classmethod 

39 def from_cfg(cls, section: SectionProxy) -> "InstanceSpec": 

40 """Parse an instance spec as read in from the enderchest config file 

41 

42 Parameters 

43 ---------- 

44 section : dict-like of str to str 

45 The section in the enderchest config as parsed by a ConfigParser 

46 

47 Returns 

48 ------- 

49 InstanceSpec 

50 The resulting InstanceSpec 

51 

52 Raises 

53 ------ 

54 KeyError 

55 If a required key is absent 

56 ValueError 

57 If a required entry cannot be parsed 

58 """ 

59 return cls( 

60 section.name, 

61 Path(section["root"]), 

62 tuple( 

63 parse_version(version.strip()) 

64 for version in cfg.parse_ini_list( 

65 section.get("minecraft-version", section.get("minecraft_version")) 

66 ) 

67 ), 

68 normalize_modloader(section.get("modloader", None))[0], 

69 tuple(cfg.parse_ini_list(section.get("groups", ""))), 

70 tuple(cfg.parse_ini_list(section.get("tags", ""))), 

71 ) 

72 

73 @property 

74 def tags(self): 

75 return tuple(sorted({*self.groups_, *self.tags_})) 

76 

77 

78def normalize_modloader(loader: str | None) -> list[str]: 

79 """Implement common modloader aliases 

80 

81 Parameters 

82 ---------- 

83 loader : str 

84 User-provided modloader name 

85 

86 Returns 

87 ------- 

88 list of str 

89 The modloader values that should be checked against to match the user's 

90 intent 

91 """ 

92 if loader is None: # this would be from the instance spec 

93 return [""] 

94 match loader.lower().replace(" ", "").replace("-", "").replace("_", "").replace( 

95 "/", "" 

96 ): 

97 case "none" | "vanilla" | "minecraftserver": 

98 return [""] 

99 case "fabric" | "fabricloader": 

100 return ["Fabric Loader"] 

101 case "quilt" | "quiltloader": 

102 return ["Quilt Loader"] 

103 case "fabricquilt" | "quiltfabric" | "fabriclike" | "fabriccompatible": 

104 return ["Fabric Loader", "Quilt Loader"] 

105 case "forge" | "forgeloader" | "minecraftforge": 

106 return ["Forge"] 

107 case _: 

108 return [loader.title()] 

109 

110 

111def equals( 

112 minecraft_root: Path, instance: InstanceSpec, other_instance: InstanceSpec 

113) -> bool: 

114 """Determine whether two instances point to the same location 

115 

116 Parameters 

117 ---------- 

118 minecraft_root : Path 

119 The starting location (the parent of where your EnderChest folder lives) 

120 instance : InstanceSpec 

121 the first instance 

122 other_instance : InstanceSpec 

123 the instance to compare it against 

124 

125 Returns 

126 ------- 

127 bool 

128 True if and only if the two instances have the same root, with regards 

129 to the provided `minecraft_root` 

130 """ 

131 path = minecraft_root / instance.root.expanduser() 

132 other_path = minecraft_root / other_instance.root.expanduser() 

133 return path.expanduser().resolve() == other_path.expanduser().resolve() 

134 

135 

136def parse_version(version_string: str) -> str: 

137 """The first release of each major Minecraft version doesn't follow strict 

138 major.minor.patch semver. This method appends the ".0" so that our version 

139 matcher doesn't mess up 

140 

141 Parameters 

142 ---------- 

143 version_string : str 

144 The version read in from the Minecraft instance's config 

145 

146 Returns 

147 ------- 

148 str 

149 Either the original version string or the original version string with 

150 ".0" tacked onto the end of it 

151 

152 Notes 

153 ----- 

154 Regex adapted straight from https://semver.org 

155 """ 

156 if re.match(r"^(0|[1-9]\d*)\.(0|[1-9]\d*)$", version_string): 

157 return version_string + ".0" 

158 return version_string 

159 

160 

161def merge(*instances: InstanceSpec) -> InstanceSpec: 

162 """Merge multiple instances, layering information from the ones provided later 

163 on top of the ones provided earlier 

164 

165 Parameters 

166 ---------- 

167 *instances : InstanceSpec 

168 The instances to combine 

169 

170 Returns 

171 ------- 

172 InstanceSpec 

173 The merged instance 

174 

175 Notes 

176 ----- 

177 The resulting merged instance will use: 

178 - the first instance's name 

179 - the union of all non-group tags 

180 - all other data from the last instance 

181 """ 

182 try: 

183 combined_instance = instances[-1] 

184 except IndexError as nothing_to_merge: 

185 raise ValueError( 

186 "Must provide at least one instance to merge" 

187 ) from nothing_to_merge 

188 tags = tuple(sorted(set(sum((instance.tags_ for instance in instances), ())))) 

189 return combined_instance._replace(name=instances[0].name, tags_=tags)