Coverage for enderchest/instance.py: 100%
49 statements
« prev ^ index » next coverage.py v7.4.1, created at 2024-02-06 16:00 +0000
« 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
7from . import config as cfg
10class InstanceSpec(NamedTuple):
11 """Specification of a Minecraft instance
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 """
30 name: str
31 root: Path
32 minecraft_versions: tuple[str, ...]
33 modloader: str
34 groups_: tuple[str, ...]
35 tags_: tuple[str, ...]
37 @classmethod
38 def from_cfg(cls, section: SectionProxy) -> "InstanceSpec":
39 """Parse an instance spec as read in from the enderchest config file
41 Parameters
42 ----------
43 section : dict-like of str to str
44 The section in the enderchest config as parsed by a ConfigParser
46 Returns
47 -------
48 InstanceSpec
49 The resulting InstanceSpec
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 )
72 @property
73 def tags(self):
74 return tuple(sorted({*self.groups_, *self.tags_}))
77def normalize_modloader(loader: str | None) -> list[str]:
78 """Implement common modloader aliases
80 Parameters
81 ----------
82 loader : str
83 User-provided modloader name
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]
110def equals(
111 minecraft_root: Path, instance: InstanceSpec, other_instance: InstanceSpec
112) -> bool:
113 """Determine whether two instances point to the same location
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
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()
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
140 Parameters
141 ----------
142 version_string : str
143 The version read in from the Minecraft instance's config
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
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
160def merge(*instances: InstanceSpec) -> InstanceSpec:
161 """Merge multiple instances, layering information from the ones provided later
162 on top of the ones provided earlier
164 Parameters
165 ----------
166 *instances : InstanceSpec
167 The instances to combine
169 Returns
170 -------
171 InstanceSpec
172 The merged instance
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)