From cfacd3ce5c27e92aec2029986f9299ab0049f4a6 Mon Sep 17 00:00:00 2001 From: Ghost Date: Thu, 1 Jan 2026 09:03:32 -0600 Subject: [PATCH 01/17] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ee81685..d1fb011 100644 --- a/README.md +++ b/README.md @@ -2,13 +2,13 @@ # pytoc -A Python package for parsing World of Warcraft addon TOC files. +A Python package for reading and writing World of Warcraft addon [TOC](https://warcraft.wiki.gg/wiki/TOC_format) files. ## Installation You can install this package via `pip`. -```py +``` pip install wow-pytoc ``` From b13e29294e3278bbabfa4d7c456b81ac423864d0 Mon Sep 17 00:00:00 2001 From: Ghost Date: Thu, 1 Jan 2026 09:03:35 -0600 Subject: [PATCH 02/17] Update CHANGELOG.md --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d78985..14e09eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +# 0.7.0 +> [!WARNING] +> ⚠️ This release contains a minimum Python version bump to 3.12 ⚠️ + +### Changed +- Migrated project management to `uv` +- Bumped minimum required Python version to 3.12 + # 0.6.0 > [!WARNING] > This release contains breaking changes, marked with ⚠️. From ac4dad7e169c09ac6dbc70ab32c6232960d1dd30 Mon Sep 17 00:00:00 2001 From: Ghost Date: Thu, 1 Jan 2026 11:00:40 -0600 Subject: [PATCH 03/17] Add Enums --- src/pytoc/enums.py | 57 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 src/pytoc/enums.py diff --git a/src/pytoc/enums.py b/src/pytoc/enums.py new file mode 100644 index 0000000..f54cd89 --- /dev/null +++ b/src/pytoc/enums.py @@ -0,0 +1,57 @@ +from enum import StrEnum + + +class TOCGameType(StrEnum): + Standard = "standard" + Mainline = "mainline" + Wowhack = "wowhack" + Wowlabs = "wowlabs" + Plunderstorm = "plunderstorm" + Classic = "classic" + Vanilla = "vanilla" + TBC = "tbc" + Wrath = "wrath" + Mists = "mists" + + +class TOCEnvironment(StrEnum): + Global = "global" + Glue = "glue" + Both = "both" + + +class TOCFamily(StrEnum): + Mainline = "Mainline" + Classic = "Classic" + + +class TOCTextLocale(StrEnum): + enUS = "enUS" + enGB = "enGB" + enTW = "enTW" + zhTW = "zhTW" + esES = "esES" + ruRU = "ruRU" + koKR = "koKR" + ptPT = "ptPT" + esMX = "esMX" + itIT = "itIT" + deDE = "deDE" + frFR = "frFR" + enCN = "enCN" + zhCN = "zhCN" + ptBR = "ptBR" + + +TOC_GAME_TYPE_TO_FAMILY = { + TOCGameType.Standard: TOCFamily.Mainline, + TOCGameType.Mainline: TOCFamily.Mainline, + TOCGameType.Wowhack: TOCFamily.Mainline, + TOCGameType.Wowlabs: TOCFamily.Mainline, + TOCGameType.Plunderstorm: TOCFamily.Mainline, + TOCGameType.Classic: TOCFamily.Classic, + TOCGameType.Vanilla: TOCFamily.Classic, + TOCGameType.TBC: TOCFamily.Classic, + TOCGameType.Wrath: TOCFamily.Classic, + TOCGameType.Mists: TOCFamily.Classic, +} From 0a955c212f84e21b350318dcd0795d6bdf02cc4a Mon Sep 17 00:00:00 2001 From: Ghost Date: Thu, 1 Jan 2026 11:00:55 -0600 Subject: [PATCH 04/17] File entry object, conditions and variables --- src/pytoc/file_entry.py | 103 ++++++++++++++++++++++++++++++++++++++++ src/pytoc/toc.py | 94 +++++++++++++++++++++++++++++------- 2 files changed, 179 insertions(+), 18 deletions(-) create mode 100644 src/pytoc/file_entry.py diff --git a/src/pytoc/file_entry.py b/src/pytoc/file_entry.py new file mode 100644 index 0000000..7b94858 --- /dev/null +++ b/src/pytoc/file_entry.py @@ -0,0 +1,103 @@ +import re + +from dataclasses import dataclass +from abc import ABC, abstractmethod +from typing import Optional + +from .enums import * + +# TOC eval context + + +@dataclass(frozen=True) +class TOCEvaluationContext: + GameType: TOCGameType + Environment: TOCEnvironment + TextLocale: TOCTextLocale + + @property + def Family(self): + try: + return TOC_GAME_TYPE_TO_FAMILY[self.GameType] + except KeyError: + raise KeyError(f"Unknown GameType specified: {self.GameType}") + + +# TOC conditions + + +class TOCCondition(ABC): + @abstractmethod + def evaluate(self, ctx: TOCEvaluationContext) -> bool: ... + + +@dataclass(frozen=True) +class TOCAllowLoad(TOCCondition): + AllowedEnvironments: frozenset[TOCEnvironment] + + def evaluate(self, ctx: TOCEvaluationContext) -> bool: + return ctx.Environment in self.AllowedEnvironments or TOCEnvironment.Both in self.AllowedEnvironments + + +class TOCAllowLoadEnvironment(TOCAllowLoad): ... + + +@dataclass(frozen=True) +class TOCAllowLoadGameType(TOCCondition): + AllowedGameTypes: frozenset[TOCGameType] + + def evaluate(self, ctx: TOCEvaluationContext) -> bool: + return ctx.GameType in self.AllowedGameTypes + + +@dataclass(frozen=True) +class TOCAllowLoadTextLocale(TOCCondition): + AllowedTextLocales: frozenset[TOCTextLocale] + + def evaluate(self, ctx: TOCEvaluationContext) -> bool: + return ctx.TextLocale in self.AllowedTextLocales + + +# TOC variables + +_TOC_VAR_PATTERN = re.compile(r"\[([A-Za-z0-9_]+)\]") + +_TOC_DEFAULT_VARIABLES = {"family": lambda ctx: ctx.Family, "game": lambda ctx: ctx.GameType, "textlocale": lambda ctx: ctx.TextLocale} + + +@dataclass +class TOCVariableResolver: + def expand(self, path: str, ctx: TOCEvaluationContext) -> str: + def replace(match: re.Match): + name = match.group(1) + try: + return _TOC_DEFAULT_VARIABLES[name.lower()](ctx) + except KeyError: + raise KeyError(f"Undefined TOC variable: {name}") + + return _TOC_VAR_PATTERN.sub(replace, path) + + +@dataclass(frozen=True) +class TOCFileEntry: + """A file found in the 'files' section of a TOC file. This represents the .lua and .xml files.""" + + RawFilePath: str + Conditions: Optional[list[TOCCondition]] = None + + def __str__(self): + return self.RawFilePath + + def resolve_path(self, resolver: TOCVariableResolver, ctx: TOCEvaluationContext) -> str: + return resolver.expand(self.RawFilePath, ctx) + + def should_load(self, ctx: TOCEvaluationContext) -> bool: + should_load = True + if self.Conditions: + for condition in self.Conditions: + condition: TOCCondition + if not condition.evaluate(ctx): + should_load = False + break + + return should_load diff --git a/src/pytoc/toc.py b/src/pytoc/toc.py index d373f92..f4c9414 100644 --- a/src/pytoc/toc.py +++ b/src/pytoc/toc.py @@ -1,9 +1,16 @@ import os +import re +import shlex from dataclasses import dataclass from typing import Any, Optional, Union +from .enums import * from .meta import TypedClass +from .file_entry import * + + +CONDITION_VARIABLE_PATTERN = re.compile(r"\[([^\]]+)\]") # characters/strings that are interpreted as falsey/truthy according to the WoW client FALSEY_CHARS = ("0", "n", "f") @@ -29,20 +36,15 @@ def StringToBoolean(string: str, defaultReturn: bool = False): # i don't like this, but this old code has forced my hand -BOOLEAN_DIRECTIVES_LOWER = ( - "defaultstate", - "onlybetaandptr", - "loadondemand", - "loadfirst", - "loadsavedvariablesfirst", - "usesecureenvironment", -) - -SAVEDVARIABLES_DIRECTIVES_LOWER = ( - "savedvariables", - "savedvariablespercharacter", - "savedvariablesmachine", -) +BOOLEAN_DIRECTIVES_LOWER = ("defaultstate", "onlybetaandptr", "loadondemand", "loadfirst", "loadsavedvariablesfirst", "usesecureenvironment") +SAVEDVARIABLES_DIRECTIVES_LOWER = ("savedvariables", "savedvariablespercharacter", "savedvariablesmachine") +CONDITION_DIRECTIVES_LOWER = ("allowload", "allowloadgametype", "allowloadenvironment", "allowloadtextlocale") +CONDITION_DIRECTIVES_TO_CLASS = { + "AllowLoad": TOCAllowLoad, + "AllowLoadGameType": TOCAllowLoadGameType, + "AllowLoadEnvironment": TOCAllowLoadEnvironment, + "AllowLoadTextLocale": TOCAllowLoadTextLocale, +} @dataclass @@ -56,7 +58,7 @@ class TOCFile(TypedClass): Title: Optional[str] = None Author: Optional[str] = None Version: Optional[str] = None - Files: Optional[list[str]] = None + Files: Optional[list[TOCFileEntry]] = None Notes: Optional[str] = None Group: Optional[str] = None Category: Optional[str] = None @@ -78,8 +80,10 @@ class TOCFile(TypedClass): DefaultState: Optional[bool] = None OnlyBetaAndPTR: Optional[bool] = None LoadSavedVariablesFirst: Optional[bool] = None - AllowLoad: Optional[str] = None # restricted to secure addons - AllowLoadGameType: Optional[str] = None + AllowLoad: Optional[TOCAllowLoad] = None # only useful to secure addons + AllowLoadGameType: Optional[TOCAllowLoadGameType] = None + AllowLoadTextLocale: Optional[TOCAllowLoadTextLocale] = None + AllowLoadEnvironment: Optional[TOCAllowLoadEnvironment] = None UseSecureEnvironment: Optional[bool] = None # restricted to secure addons AdditionalFields: Optional[dict[str, Any]] = None # this is a dict of x- fields @@ -194,6 +198,8 @@ def set_field(self, directive: str, value: Any): self.__setattr__(directive, StringToBoolean(value, False)) elif directive_lower in SAVEDVARIABLES_DIRECTIVES_LOWER: self.add_saved_variable(directive, value) + elif directive_lower in CONDITION_DIRECTIVES_LOWER: + self.add_conditional_field(directive, value) else: self.__setattr__(directive, value) @@ -234,13 +240,65 @@ def add_additional_field(self, directive: str, value: Any): self.AdditionalFields[directive] = value + def split_file_path_and_conditions(self, line: str) -> tuple[str, list[str]]: + line = line.strip() + depth = 0 + + for i, char in enumerate(line): + if char == "[": + depth += 1 + elif char == "]": + depth -= 1 + elif char.isspace() and depth == 0: + path = line[:i] + rest = line[i:].strip() + return path, CONDITION_VARIABLE_PATTERN.findall(rest) + + return line, [] + + def parse_condition(self, text: str) -> Optional[TOCCondition]: + name, *rest = text.split(None, 1) + args = [] + + if rest: + args = [a.strip() for a in rest[0].split(",")] + + condition_class = CONDITION_DIRECTIVES_TO_CLASS.get(name) + + if condition_class: + return condition_class(frozenset(args)) + + return None + + def parse_file_line(self, line: str): + raw_path, condition_texts = self.split_file_path_and_conditions(line) + + conditions = [self.parse_condition(text) for text in condition_texts] + + if not conditions: + return TOCFileEntry(raw_path) + + return TOCFileEntry(raw_path, conditions) + def add_file(self, file_name: str): if not self.has_attr("_files"): self.Files = [] - self.Files.append(file_name) + file_entry = self.parse_file_line(file_name) + self.Files.append(file_entry) def add_saved_variable(self, directive: str, value: Union[str, list[str]]): if isinstance(value, str): value = [value] setattr(self, directive, value) + + def add_conditional_field(self, directive: str, value: Union[str, list[str]]): + if isinstance(value, str): + value = [value] + + try: + directive_class = CONDITION_DIRECTIVES_TO_CLASS[directive]({*value}) + except KeyError: + raise KeyError(f"Unknown conditional directive: {directive}") + + setattr(self, directive, directive_class) From aa0c625a2ddfe230752919e699291ab910661d7f Mon Sep 17 00:00:00 2001 From: Ghost Date: Thu, 1 Jan 2026 11:40:36 -0600 Subject: [PATCH 05/17] Update enums.py --- src/pytoc/enums.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pytoc/enums.py b/src/pytoc/enums.py index f54cd89..09dda0c 100644 --- a/src/pytoc/enums.py +++ b/src/pytoc/enums.py @@ -15,9 +15,9 @@ class TOCGameType(StrEnum): class TOCEnvironment(StrEnum): - Global = "global" - Glue = "glue" - Both = "both" + Global = "Global" + Glue = "Glue" + Both = "Both" class TOCFamily(StrEnum): From 52f219daa7228f23d840b147dd7dce488605533d Mon Sep 17 00:00:00 2001 From: Ghost Date: Thu, 1 Jan 2026 11:43:50 -0600 Subject: [PATCH 06/17] Make exports work --- src/pytoc/file_entry.py | 36 ++++++++++++++++++++++++++++-------- src/pytoc/meta.py | 3 +++ src/pytoc/toc.py | 18 +++++++++++++++--- 3 files changed, 46 insertions(+), 11 deletions(-) diff --git a/src/pytoc/file_entry.py b/src/pytoc/file_entry.py index 7b94858..a50c727 100644 --- a/src/pytoc/file_entry.py +++ b/src/pytoc/file_entry.py @@ -2,7 +2,7 @@ from dataclasses import dataclass from abc import ABC, abstractmethod -from typing import Optional +from typing import Optional, Any from .enums import * @@ -27,35 +27,45 @@ def Family(self): class TOCCondition(ABC): + AllowedValues: frozenset[Any] + ExportName: str + @abstractmethod def evaluate(self, ctx: TOCEvaluationContext) -> bool: ... + def export(self) -> str: + return f"[{self.ExportName} " + ", ".join(self.AllowedValues) + "]" + @dataclass(frozen=True) class TOCAllowLoad(TOCCondition): - AllowedEnvironments: frozenset[TOCEnvironment] + AllowedValues: frozenset[TOCEnvironment] + ExportName: str = "AllowLoad" def evaluate(self, ctx: TOCEvaluationContext) -> bool: - return ctx.Environment in self.AllowedEnvironments or TOCEnvironment.Both in self.AllowedEnvironments + return ctx.Environment in self.AllowedValues or TOCEnvironment.Both in self.AllowedValues -class TOCAllowLoadEnvironment(TOCAllowLoad): ... +class TOCAllowLoadEnvironment(TOCAllowLoad): + ExportName: str = "AllowLoadEnvironment" @dataclass(frozen=True) class TOCAllowLoadGameType(TOCCondition): - AllowedGameTypes: frozenset[TOCGameType] + AllowedValues: frozenset[TOCGameType] + ExportName: str = "AllowLoadGameType" def evaluate(self, ctx: TOCEvaluationContext) -> bool: - return ctx.GameType in self.AllowedGameTypes + return ctx.GameType in self.AllowedValues @dataclass(frozen=True) class TOCAllowLoadTextLocale(TOCCondition): - AllowedTextLocales: frozenset[TOCTextLocale] + AllowedValues: frozenset[TOCTextLocale] + ExportName: str = "AllowLoadTextLocale" def evaluate(self, ctx: TOCEvaluationContext) -> bool: - return ctx.TextLocale in self.AllowedTextLocales + return ctx.TextLocale in self.AllowedValues # TOC variables @@ -101,3 +111,13 @@ def should_load(self, ctx: TOCEvaluationContext) -> bool: break return should_load + + def export(self) -> str: + path = self.RawFilePath + + if self.Conditions: + for condition in self.Conditions: + condition_str = f" {condition.export()}" + path += condition_str + + return path.strip() diff --git a/src/pytoc/meta.py b/src/pytoc/meta.py index 2a88913..9729ed0 100644 --- a/src/pytoc/meta.py +++ b/src/pytoc/meta.py @@ -33,6 +33,9 @@ def _cast(self, value, t): origin = get_origin(t) args = get_args(t) if origin is None: + if isinstance(value, t): + return value + return t(value) if origin is Union: for t in get_args(t): diff --git a/src/pytoc/toc.py b/src/pytoc/toc.py index f4c9414..7839605 100644 --- a/src/pytoc/toc.py +++ b/src/pytoc/toc.py @@ -107,7 +107,7 @@ def export(self, file_path: str, overwrite: bool = False): if _files is None or len(_files) == 0: continue - files.append("\n".join(_files)) + files.append("\n".join([f.export() for f in _files])) elif directive == "Dependencies": deps = self.Dependencies if deps is None or len(deps) == 0: @@ -134,11 +134,14 @@ def export(self, file_path: str, overwrite: bool = False): if data is None: continue - if isinstance(data, list) and len(data) > 0: + if isinstance(data, TOCCondition): + lines.append(f"## {directive}: {', '.join(data.AllowedValues)}\n") + elif isinstance(data, list) and len(data) > 0: str_data = [str(v) for v in data] lines.append(f"## {directive}: " + ", ".join(str_data) + "\n") else: - if directive.lower() in BOOLEAN_DIRECTIVES_LOWER: + directive_lower = directive.lower() + if directive_lower in BOOLEAN_DIRECTIVES_LOWER: # convert our boolean directive to a 1 or 0 data = "1" if data else "0" @@ -302,3 +305,12 @@ def add_conditional_field(self, directive: str, value: Union[str, list[str]]): raise KeyError(f"Unknown conditional directive: {directive}") setattr(self, directive, directive_class) + + def get_raw_files(self) -> list[str]: + """Returns a list of raw file paths. (no variable or conditional parsing done)""" + + raw_files = [] + for file in self.Files: + raw_files.append(file.export()) + + return raw_files From 206ed576ecc7d3967fac90a85c1b15b23149b786 Mon Sep 17 00:00:00 2001 From: Ghost Date: Thu, 1 Jan 2026 11:43:57 -0600 Subject: [PATCH 07/17] Update tests --- tests/test_toc.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_toc.py b/tests/test_toc.py index b2971e3..9822784 100644 --- a/tests/test_toc.py +++ b/tests/test_toc.py @@ -28,7 +28,7 @@ def test_parser(): assert file.AddonCompartmentFuncOnEnter == "GHOST_OnAddonCompartmentEnter" assert file.AddonCompartmentFuncOnLeave == "GHOST_OnAddonCompartmentLeave" assert file.AdditionalFields["X-Website"] == "https://ghst.tools" - assert file.Files == [ + assert file.get_raw_files() == [ "Libs/LibStub/LibStub.lua", "Libs/CallbackHandler-1.0/CallbackHandler-1.0.xml", "Libs/LibDataBroker-1.1/LibDataBroker-1.1.lua", @@ -147,4 +147,4 @@ def test_read_export(): assert toc.LocalizedCategory["zhTW"] == "角色扮演" assert toc.OnlyBetaAndPTR == True assert toc.DefaultState == True - assert toc.Files == ["file1.lua", "file2.xml"] + assert toc.get_raw_files() == ["file1.lua", "file2.xml"] From 2a0bd19289d46d6df3ad7a9ab4633d72a8b8a7a6 Mon Sep 17 00:00:00 2001 From: Ghost Date: Thu, 1 Jan 2026 11:56:45 -0600 Subject: [PATCH 08/17] Add ClientType, change file path to `pathlib.Path` --- src/pytoc/toc.py | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/src/pytoc/toc.py b/src/pytoc/toc.py index 7839605..d055ee3 100644 --- a/src/pytoc/toc.py +++ b/src/pytoc/toc.py @@ -1,7 +1,7 @@ import os import re -import shlex +from pathlib import Path from dataclasses import dataclass from typing import Any, Optional, Union @@ -54,6 +54,7 @@ class Dependency: class TOCFile(TypedClass): + ClientType: Optional[TOCGameType] = None # target client for client-specific TOC files. i.e. MyAddon_Standard.toc Interface: Optional[Union[int, list[int]]] = None Title: Optional[str] = None Author: Optional[str] = None @@ -87,14 +88,29 @@ class TOCFile(TypedClass): UseSecureEnvironment: Optional[bool] = None # restricted to secure addons AdditionalFields: Optional[dict[str, Any]] = None # this is a dict of x- fields - def __init__(self, file_path: Optional[str] = None): + def __init__(self, file_path: Optional[Union[Path, str]] = None): super().__init__() if file_path is not None: + if not isinstance(file_path, Path): + file_path = Path(file_path) + self.parse_toc_file(file_path) def has_attr(self, attr: str) -> bool: return attr in self.__dict__ + def get_target_client_from_path(self, file_path: Path) -> Optional[TOCGameType]: + str_path = str(file_path) + if not "_" in str_path: + return None + + path_split = str_path.split("_") + suffix = path_split[-1].removesuffix(".toc") + if suffix.lower() in TOCGameType: + return TOCGameType[suffix.title()] + + return None + def export(self, file_path: str, overwrite: bool = False): if os.path.exists(file_path) and not overwrite: raise FileExistsError("Destination file already exists. To overwrite, set overwrite=True") @@ -153,10 +169,12 @@ def export(self, file_path: str, overwrite: bool = False): with open(file_path, "w", encoding="utf-8") as f: f.writelines(lines) - def parse_toc_file(self, file_path: str): - if not os.path.exists(file_path): + def parse_toc_file(self, file_path: Path): + if not file_path.exists(): raise FileNotFoundError("TOC file not found") + self.ClientType = self.get_target_client_from_path(file_path) + # toc files should be utf-8 encoded with open(file_path, "r", encoding="utf-8") as f: toc_file = f.read() From f7340dc4439df5f1a4c69e6b4b205c133cf3aabc Mon Sep 17 00:00:00 2001 From: Ghost Date: Thu, 1 Jan 2026 12:10:44 -0600 Subject: [PATCH 09/17] Change AllowLoadEnvironment to not be so childish --- src/pytoc/file_entry.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/pytoc/file_entry.py b/src/pytoc/file_entry.py index a50c727..940b543 100644 --- a/src/pytoc/file_entry.py +++ b/src/pytoc/file_entry.py @@ -46,9 +46,14 @@ def evaluate(self, ctx: TOCEvaluationContext) -> bool: return ctx.Environment in self.AllowedValues or TOCEnvironment.Both in self.AllowedValues -class TOCAllowLoadEnvironment(TOCAllowLoad): +@dataclass(frozen=True) +class TOCAllowLoadEnvironment(TOCCondition): + AllowedValues: frozenset[TOCEnvironment] ExportName: str = "AllowLoadEnvironment" + def evaluate(self, ctx: TOCEvaluationContext) -> bool: + return ctx.Environment in self.AllowedValues or TOCEnvironment.Both in self.AllowedValues + @dataclass(frozen=True) class TOCAllowLoadGameType(TOCCondition): From dcba15931c61687b8d309a0d540b0904b71c0076 Mon Sep 17 00:00:00 2001 From: Ghost Date: Thu, 1 Jan 2026 12:11:32 -0600 Subject: [PATCH 10/17] Don't export ClientType --- src/pytoc/toc.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/pytoc/toc.py b/src/pytoc/toc.py index d055ee3..82da7d3 100644 --- a/src/pytoc/toc.py +++ b/src/pytoc/toc.py @@ -9,6 +9,7 @@ from .meta import TypedClass from .file_entry import * +DO_NOT_EXPORT_FIELDS = {"ClientType"} CONDITION_VARIABLE_PATTERN = re.compile(r"\[([^\]]+)\]") @@ -118,6 +119,9 @@ def export(self, file_path: str, overwrite: bool = False): lines = [] files = [] for directive in self.__annotations__: + if directive in DO_NOT_EXPORT_FIELDS: + continue + if directive == "Files": _files = self.Files if _files is None or len(_files) == 0: From 6aa827f1e06eee230b3e5d0c28b478075e7e0b65 Mon Sep 17 00:00:00 2001 From: Ghost Date: Thu, 1 Jan 2026 12:17:22 -0600 Subject: [PATCH 11/17] Remove dumb resolver class --- src/pytoc/file_entry.py | 24 +++++++++--------------- 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/src/pytoc/file_entry.py b/src/pytoc/file_entry.py index 940b543..5589996 100644 --- a/src/pytoc/file_entry.py +++ b/src/pytoc/file_entry.py @@ -80,19 +80,6 @@ def evaluate(self, ctx: TOCEvaluationContext) -> bool: _TOC_DEFAULT_VARIABLES = {"family": lambda ctx: ctx.Family, "game": lambda ctx: ctx.GameType, "textlocale": lambda ctx: ctx.TextLocale} -@dataclass -class TOCVariableResolver: - def expand(self, path: str, ctx: TOCEvaluationContext) -> str: - def replace(match: re.Match): - name = match.group(1) - try: - return _TOC_DEFAULT_VARIABLES[name.lower()](ctx) - except KeyError: - raise KeyError(f"Undefined TOC variable: {name}") - - return _TOC_VAR_PATTERN.sub(replace, path) - - @dataclass(frozen=True) class TOCFileEntry: """A file found in the 'files' section of a TOC file. This represents the .lua and .xml files.""" @@ -103,8 +90,15 @@ class TOCFileEntry: def __str__(self): return self.RawFilePath - def resolve_path(self, resolver: TOCVariableResolver, ctx: TOCEvaluationContext) -> str: - return resolver.expand(self.RawFilePath, ctx) + def resolve_path(self, ctx: TOCEvaluationContext) -> str: + def replace(match: re.Match): + name = match.group(1) + try: + return _TOC_DEFAULT_VARIABLES[name.lower()](ctx) + except KeyError: + raise KeyError(f"Undefined file path variable: {name}") + + return _TOC_VAR_PATTERN.sub(replace, self.RawFilePath) def should_load(self, ctx: TOCEvaluationContext) -> bool: should_load = True From f2baef7e58b1cce2de4f36465b2dd1a255a4e14d Mon Sep 17 00:00:00 2001 From: Ghost Date: Thu, 1 Jan 2026 12:17:32 -0600 Subject: [PATCH 12/17] Better word --- src/pytoc/file_entry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pytoc/file_entry.py b/src/pytoc/file_entry.py index 5589996..1c69f3d 100644 --- a/src/pytoc/file_entry.py +++ b/src/pytoc/file_entry.py @@ -82,7 +82,7 @@ def evaluate(self, ctx: TOCEvaluationContext) -> bool: @dataclass(frozen=True) class TOCFileEntry: - """A file found in the 'files' section of a TOC file. This represents the .lua and .xml files.""" + """A file found in the 'files' section of a TOC file. Represents the .lua and .xml files.""" RawFilePath: str Conditions: Optional[list[TOCCondition]] = None From 6aefce2fcd8d7fbfa3f2611f7dfcd23096271c83 Mon Sep 17 00:00:00 2001 From: Ghost Date: Thu, 1 Jan 2026 12:17:41 -0600 Subject: [PATCH 13/17] use `pathlib.Path`, bro --- src/pytoc/toc.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pytoc/toc.py b/src/pytoc/toc.py index 82da7d3..9bdede0 100644 --- a/src/pytoc/toc.py +++ b/src/pytoc/toc.py @@ -113,7 +113,8 @@ def get_target_client_from_path(self, file_path: Path) -> Optional[TOCGameType]: return None def export(self, file_path: str, overwrite: bool = False): - if os.path.exists(file_path) and not overwrite: + file_path = Path(file_path) + if file_path.exists() and not overwrite: raise FileExistsError("Destination file already exists. To overwrite, set overwrite=True") lines = [] From d11887b7b95fd42878492dddebe0400e0db7abf0 Mon Sep 17 00:00:00 2001 From: Ghost Date: Thu, 1 Jan 2026 12:18:04 -0600 Subject: [PATCH 14/17] Remove unused import --- src/pytoc/toc.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pytoc/toc.py b/src/pytoc/toc.py index 9bdede0..69f9293 100644 --- a/src/pytoc/toc.py +++ b/src/pytoc/toc.py @@ -1,4 +1,3 @@ -import os import re from pathlib import Path From dd9e042cefe7f0f456a8e8dae2e856c5e76d280d Mon Sep 17 00:00:00 2001 From: Ghost Date: Thu, 1 Jan 2026 12:21:51 -0600 Subject: [PATCH 15/17] Update CHANGELOG.md --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 14e09eb..f645717 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ > [!WARNING] > ⚠️ This release contains a minimum Python version bump to 3.12 ⚠️ +### Added +- Added support for the new (not new) TOC variables and conditionals +- Added support for client-specific TOC files based on the file name + ### Changed - Migrated project management to `uv` - Bumped minimum required Python version to 3.12 From 95c5b34130d4da120d4beace9966f7557c1d1962 Mon Sep 17 00:00:00 2001 From: Ghost Date: Thu, 1 Jan 2026 12:22:40 -0600 Subject: [PATCH 16/17] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f645717..fb5d581 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Added - Added support for the new (not new) TOC variables and conditionals - Added support for client-specific TOC files based on the file name + - You can find this value, if specified, in the `TOCFile.ClientType` attribute. This attribute does not get exported ### Changed - Migrated project management to `uv` From abbd1b4e6e8635e1a78202c98e937fa1a065570c Mon Sep 17 00:00:00 2001 From: Ghost Date: Thu, 1 Jan 2026 12:26:17 -0600 Subject: [PATCH 17/17] the family type hint --- src/pytoc/file_entry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pytoc/file_entry.py b/src/pytoc/file_entry.py index 1c69f3d..7030615 100644 --- a/src/pytoc/file_entry.py +++ b/src/pytoc/file_entry.py @@ -16,7 +16,7 @@ class TOCEvaluationContext: TextLocale: TOCTextLocale @property - def Family(self): + def Family(self) -> TOCFamily: try: return TOC_GAME_TYPE_TO_FAMILY[self.GameType] except KeyError: