Coverage for enderchest/instance.py: 100%
49 statements
« prev ^ index » next coverage.py v7.7.1, created at 2025-03-28 20:32 +0000
« prev ^ index » next coverage.py v7.7.1, created at 2025-03-28 20:32 +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(
66 "minecraft-version", section.get("minecraft_version", "")
67 )
68 )
69 ),
70 normalize_modloader(section.get("modloader", None))[0],
71 tuple(cfg.parse_ini_list(section.get("groups", ""))),
72 tuple(cfg.parse_ini_list(section.get("tags", ""))),
73 )
75 @property
76 def tags(self):
77 return tuple(sorted({*self.groups_, *self.tags_}))
80def normalize_modloader(loader: str | None) -> list[str]:
81 """Implement common modloader aliases
83 Parameters
84 ----------
85 loader : str
86 User-provided modloader name
88 Returns
89 -------
90 list of str
91 The modloader values that should be checked against to match the user's
92 intent
93 """
94 if loader is None: # this would be from the instance spec
95 return [""]
96 match loader.lower().replace(" ", "").replace("-", "").replace("_", "").replace(
97 "/", ""
98 ):
99 case "none" | "vanilla" | "minecraftserver":
100 return [""]
101 case "fabric" | "fabricloader":
102 return ["Fabric Loader"]
103 case "quilt" | "quiltloader":
104 return ["Quilt Loader"]
105 case "fabricquilt" | "quiltfabric" | "fabriclike" | "fabriccompatible":
106 return ["Fabric Loader", "Quilt Loader"]
107 case "forge" | "forgeloader" | "minecraftforge":
108 return ["Forge"]
109 case _:
110 return [loader.title()]
113def equals(
114 minecraft_root: Path, instance: InstanceSpec, other_instance: InstanceSpec
115) -> bool:
116 """Determine whether two instances point to the same location
118 Parameters
119 ----------
120 minecraft_root : Path
121 The starting location (the parent of where your EnderChest folder lives)
122 instance : InstanceSpec
123 the first instance
124 other_instance : InstanceSpec
125 the instance to compare it against
127 Returns
128 -------
129 bool
130 True if and only if the two instances have the same root, with regards
131 to the provided `minecraft_root`
132 """
133 path = minecraft_root / instance.root.expanduser()
134 other_path = minecraft_root / other_instance.root.expanduser()
135 return path.expanduser().resolve() == other_path.expanduser().resolve()
138def parse_version(version_string: str) -> str:
139 """The first release of each major Minecraft version doesn't follow strict
140 major.minor.patch semver. This method appends the ".0" so that our version
141 matcher doesn't mess up
143 Parameters
144 ----------
145 version_string : str
146 The version read in from the Minecraft instance's config
148 Returns
149 -------
150 str
151 Either the original version string or the original version string with
152 ".0" tacked onto the end of it
154 Notes
155 -----
156 Regex adapted straight from https://semver.org
157 """
158 if re.match(r"^(0|[1-9]\d*)\.(0|[1-9]\d*)$", version_string):
159 return version_string + ".0"
160 return version_string
163def merge(*instances: InstanceSpec) -> InstanceSpec:
164 """Merge multiple instances, layering information from the ones provided later
165 on top of the ones provided earlier
167 Parameters
168 ----------
169 *instances : InstanceSpec
170 The instances to combine
172 Returns
173 -------
174 InstanceSpec
175 The merged instance
177 Notes
178 -----
179 The resulting merged instance will use:
180 - the first instance's name
181 - the union of all non-group tags
182 - all other data from the last instance
183 """
184 try:
185 combined_instance = instances[-1]
186 except IndexError as nothing_to_merge:
187 raise ValueError(
188 "Must provide at least one instance to merge"
189 ) from nothing_to_merge
190 tags = tuple(sorted(set(sum((instance.tags_ for instance in instances), ()))))
191 return combined_instance._replace(name=instances[0].name, tags_=tags)