Coverage for enderchest/instance.py: 100%
49 statements
« prev ^ index » next coverage.py v7.5.0, created at 2024-05-04 01:41 +0000
« prev ^ index » next coverage.py v7.5.0, created at 2024-05-04 01:41 +0000
1"""Specification of a Minecraft instance"""
3import re
4from configparser import SectionProxy
5from pathlib import Path
6from typing import NamedTuple
8from . import config as cfg
11class InstanceSpec(NamedTuple):
12 """Specification of a Minecraft instance
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 """
31 name: str
32 root: Path
33 minecraft_versions: tuple[str, ...]
34 modloader: str
35 groups_: tuple[str, ...]
36 tags_: tuple[str, ...]
38 @classmethod
39 def from_cfg(cls, section: SectionProxy) -> "InstanceSpec":
40 """Parse an instance spec as read in from the enderchest config file
42 Parameters
43 ----------
44 section : dict-like of str to str
45 The section in the enderchest config as parsed by a ConfigParser
47 Returns
48 -------
49 InstanceSpec
50 The resulting InstanceSpec
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 )
73 @property
74 def tags(self):
75 return tuple(sorted({*self.groups_, *self.tags_}))
78def normalize_modloader(loader: str | None) -> list[str]:
79 """Implement common modloader aliases
81 Parameters
82 ----------
83 loader : str
84 User-provided modloader name
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()]
111def equals(
112 minecraft_root: Path, instance: InstanceSpec, other_instance: InstanceSpec
113) -> bool:
114 """Determine whether two instances point to the same location
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
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()
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
141 Parameters
142 ----------
143 version_string : str
144 The version read in from the Minecraft instance's config
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
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
161def merge(*instances: InstanceSpec) -> InstanceSpec:
162 """Merge multiple instances, layering information from the ones provided later
163 on top of the ones provided earlier
165 Parameters
166 ----------
167 *instances : InstanceSpec
168 The instances to combine
170 Returns
171 -------
172 InstanceSpec
173 The merged instance
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)