Coverage for enderchest/instance.py: 100%

49 statements  

« prev     ^ index     » next       coverage.py v7.4.1, created at 2024-02-06 16:00 +0000

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

2import re 

3from configparser import SectionProxy 

4from pathlib import Path 

5from typing import NamedTuple 

6 

7from . import config as cfg 

8 

9 

10class InstanceSpec(NamedTuple): 

11 """Specification of a Minecraft instance 

12 

13 Parameters 

14 ---------- 

15 name : str 

16 The "display name" for the instance 

17 root : Path 

18 The path to its ".minecraft" folder 

19 minecraft_versions : list-like of str 

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

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

22 your assets together across all profiles 

23 modloader : str 

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

25 tags : list-like of str 

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

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

28 """ 

29 

30 name: str 

31 root: Path 

32 minecraft_versions: tuple[str, ...] 

33 modloader: str 

34 groups_: tuple[str, ...] 

35 tags_: tuple[str, ...] 

36 

37 @classmethod 

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

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

40 

41 Parameters 

42 ---------- 

43 section : dict-like of str to str 

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

45 

46 Returns 

47 ------- 

48 InstanceSpec 

49 The resulting InstanceSpec 

50 

51 Raises 

52 ------ 

53 KeyError 

54 If a required key is absent 

55 ValueError 

56 If a required entry cannot be parsed 

57 """ 

58 return cls( 

59 section.name, 

60 Path(section["root"]), 

61 tuple( 

62 parse_version(version.strip()) 

63 for version in cfg.parse_ini_list( 

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

65 ) 

66 ), 

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

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

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

70 ) 

71 

72 @property 

73 def tags(self): 

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

75 

76 

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

78 """Implement common modloader aliases 

79 

80 Parameters 

81 ---------- 

82 loader : str 

83 User-provided modloader name 

84 

85 Returns 

86 ------- 

87 list of str 

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

89 intent 

90 """ 

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

92 return [""] 

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

94 "/", "" 

95 ): 

96 case "none" | "vanilla": 

97 return [""] 

98 case "fabric" | "fabricloader": 

99 return ["Fabric Loader"] 

100 case "quilt" | "quiltloader": 

101 return ["Quilt Loader"] 

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

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

104 case "forge" | "forgeloader" | "minecraftforge": 

105 return ["Forge"] 

106 case _: 

107 return [loader] 

108 

109 

110def equals( 

111 minecraft_root: Path, instance: InstanceSpec, other_instance: InstanceSpec 

112) -> bool: 

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

114 

115 Parameters 

116 ---------- 

117 minecraft_root : Path 

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

119 instance : InstanceSpec 

120 the first instance 

121 other_instance : InstanceSpec 

122 the instance to compare it against 

123 

124 Returns 

125 ------- 

126 bool 

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

128 to the provided `minecraft_root` 

129 """ 

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

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

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

133 

134 

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

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

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

138 matcher doesn't mess up 

139 

140 Parameters 

141 ---------- 

142 version_string : str 

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

144 

145 Returns 

146 ------- 

147 str 

148 Either the original version string or the original version string with 

149 ".0" tacked onto the end of it 

150 

151 Notes 

152 ----- 

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

154 """ 

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

156 return version_string + ".0" 

157 return version_string 

158 

159 

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

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

162 on top of the ones provided earlier 

163 

164 Parameters 

165 ---------- 

166 *instances : InstanceSpec 

167 The instances to combine 

168 

169 Returns 

170 ------- 

171 InstanceSpec 

172 The merged instance 

173 

174 Notes 

175 ----- 

176 The resulting merged instance will use: 

177 - the first instance's name 

178 - the union of all non-group tags 

179 - all other data from the last instance 

180 """ 

181 try: 

182 combined_instance = instances[-1] 

183 except IndexError as nothing_to_merge: 

184 raise ValueError( 

185 "Must provide at least one instance to merge" 

186 ) from nothing_to_merge 

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

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