From c10bf78e851d0c04a5e8ce88b1593167c6874a68 Mon Sep 17 00:00:00 2001 From: Ghost Date: Thu, 1 Jan 2026 12:50:25 -0600 Subject: [PATCH 1/8] Add addon load testing --- src/pytoc/context.py | 30 ++++++++++++++++++++++++++++++ src/pytoc/enums.py | 10 +++++++++- src/pytoc/file_entry.py | 18 +----------------- src/pytoc/toc.py | 22 ++++++++++++++++++++++ 4 files changed, 62 insertions(+), 18 deletions(-) create mode 100644 src/pytoc/context.py diff --git a/src/pytoc/context.py b/src/pytoc/context.py new file mode 100644 index 0000000..29e23ea --- /dev/null +++ b/src/pytoc/context.py @@ -0,0 +1,30 @@ +from dataclasses import dataclass, field + +from .enums import * + + +@dataclass +class TOCEvaluationContext: + GameType: TOCGameType + Environment: TOCEnvironment + TextLocale: TOCTextLocale + LoadedAddons: dict[str, bool] = field(default_factory=dict) + + @property + def Family(self) -> TOCFamily: + try: + return TOC_GAME_TYPE_TO_FAMILY[self.GameType] + except KeyError: + raise KeyError(f"Unknown GameType specified: {self.GameType}") + + def load_addon(self, addon_name: str): + self.LoadedAddons[addon_name] = True + + def unload_addon(self, addon_name: str): + try: + self.LoadedAddons.pop(addon_name) + except KeyError: + ... + + def is_addon_loaded(self, addon_name: str) -> bool: + return self.LoadedAddons.get(addon_name, False) diff --git a/src/pytoc/enums.py b/src/pytoc/enums.py index 09dda0c..52ed697 100644 --- a/src/pytoc/enums.py +++ b/src/pytoc/enums.py @@ -1,4 +1,4 @@ -from enum import StrEnum +from enum import StrEnum, Enum class TOCGameType(StrEnum): @@ -55,3 +55,11 @@ class TOCTextLocale(StrEnum): TOCGameType.Wrath: TOCFamily.Classic, TOCGameType.Mists: TOCFamily.Classic, } + + +class TOCAddonLoadError(Enum): + Success = 1 + WrongGameType = 2 + WrongEnvironment = 3 + WrongTextLocale = 4 + MissingDependency = 5 diff --git a/src/pytoc/file_entry.py b/src/pytoc/file_entry.py index 7030615..4713891 100644 --- a/src/pytoc/file_entry.py +++ b/src/pytoc/file_entry.py @@ -5,23 +5,7 @@ from typing import Optional, Any from .enums import * - -# TOC eval context - - -@dataclass(frozen=True) -class TOCEvaluationContext: - GameType: TOCGameType - Environment: TOCEnvironment - TextLocale: TOCTextLocale - - @property - def Family(self) -> TOCFamily: - try: - return TOC_GAME_TYPE_TO_FAMILY[self.GameType] - except KeyError: - raise KeyError(f"Unknown GameType specified: {self.GameType}") - +from .context import TOCEvaluationContext # TOC conditions diff --git a/src/pytoc/toc.py b/src/pytoc/toc.py index 69f9293..a18b4a5 100644 --- a/src/pytoc/toc.py +++ b/src/pytoc/toc.py @@ -336,3 +336,25 @@ def get_raw_files(self) -> list[str]: raw_files.append(file.export()) return raw_files + + def can_load_addon(self, context: TOCEvaluationContext) -> tuple[bool, TOCAddonLoadError]: + if self.Dependencies and len(self.Dependencies) > 0: + deps_fulfilled = True + for dep in self.Dependencies: + if dep.Required and not context.is_addon_loaded(dep.Name): + deps_fulfilled = False + break + + if not deps_fulfilled: + return False, TOCAddonLoadError.MissingDependency + + if self.AllowLoad and not self.AllowLoad.evaluate(context): + return False, TOCAddonLoadError.WrongEnvironment + elif self.AllowLoadEnvironment and not self.AllowLoadEnvironment.evaluate(context): + return False, TOCAddonLoadError.WrongEnvironment + elif self.AllowLoadGameType and not self.AllowLoadGameType.evaluate(context): + return False, TOCAddonLoadError.WrongGameType + elif self.AllowLoadTextLocale and not self.AllowLoadTextLocale.evaluate(context): + return False, TOCAddonLoadError.WrongTextLocale + + return True, TOCAddonLoadError.Success From 687acfb6e91d17d574b85fa35495651cfe1d924b Mon Sep 17 00:00:00 2001 From: Ghost Date: Thu, 1 Jan 2026 12:52:25 -0600 Subject: [PATCH 2/8] Rename Dependency class --- src/pytoc/toc.py | 8 ++++---- tests/test_toc.py | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/pytoc/toc.py b/src/pytoc/toc.py index a18b4a5..b1676d3 100644 --- a/src/pytoc/toc.py +++ b/src/pytoc/toc.py @@ -48,7 +48,7 @@ def StringToBoolean(string: str, defaultReturn: bool = False): @dataclass -class Dependency: +class TOCDependency: Name: str Required: bool @@ -77,7 +77,7 @@ class TOCFile(TypedClass): LoadWith: Optional[list[str]] = None LoadFirst: Optional[bool] = None LoadManagers: Optional[list[str]] = None - Dependencies: Optional[list[Dependency]] = None + Dependencies: Optional[list[TOCDependency]] = None DefaultState: Optional[bool] = None OnlyBetaAndPTR: Optional[bool] = None LoadSavedVariablesFirst: Optional[bool] = None @@ -234,9 +234,9 @@ def add_dependency(self, name: str, required: bool): if isinstance(name, list): for _name in name: - self.Dependencies.append(Dependency(_name, required)) + self.Dependencies.append(TOCDependency(_name, required)) else: - self.Dependencies.append(Dependency(name, required)) + self.Dependencies.append(TOCDependency(name, required)) def add_localized_directive(self, directive: str, value: str, locale: str): # localized directive will be accessible via the `.Localized` attribute diff --git a/tests/test_toc.py b/tests/test_toc.py index 9822784..8972ff4 100644 --- a/tests/test_toc.py +++ b/tests/test_toc.py @@ -1,7 +1,7 @@ import os import pytest -from pytoc import TOCFile, Dependency +from pytoc import TOCFile, TOCDependency PWD = os.path.dirname(os.path.realpath(__file__)) @@ -72,7 +72,7 @@ def test_parser(): } for dep in file.Dependencies: - dep: Dependency + dep: TOCDependency if expected_deps[dep.Name] == dep.Required: expected_deps.pop(dep.Name) From bebbefe978b3195b3983aae9d005ff2970a2605b Mon Sep 17 00:00:00 2001 From: Ghost Date: Thu, 1 Jan 2026 13:44:38 -0600 Subject: [PATCH 3/8] Add FilePath to TOC object --- src/pytoc/toc.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/pytoc/toc.py b/src/pytoc/toc.py index b1676d3..088f954 100644 --- a/src/pytoc/toc.py +++ b/src/pytoc/toc.py @@ -8,7 +8,7 @@ from .meta import TypedClass from .file_entry import * -DO_NOT_EXPORT_FIELDS = {"ClientType"} +DO_NOT_EXPORT_FIELDS = {"ClientType", "FilePath"} CONDITION_VARIABLE_PATTERN = re.compile(r"\[([^\]]+)\]") @@ -54,6 +54,7 @@ class TOCDependency: class TOCFile(TypedClass): + FilePath: Optional[Path] = None 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 @@ -94,6 +95,7 @@ def __init__(self, file_path: Optional[Union[Path, str]] = None): if not isinstance(file_path, Path): file_path = Path(file_path) + self.FilePath = file_path self.parse_toc_file(file_path) def has_attr(self, attr: str) -> bool: @@ -107,7 +109,10 @@ def get_target_client_from_path(self, file_path: Path) -> Optional[TOCGameType]: path_split = str_path.split("_") suffix = path_split[-1].removesuffix(".toc") if suffix.lower() in TOCGameType: - return TOCGameType[suffix.title()] + if suffix.title() in TOCGameType._member_names_: + return TOCGameType[suffix.title()] + elif suffix.upper() in TOCGameType._member_names_: + return TOCGameType[suffix.upper()] return None From 11cf393a3082307e4736f436c274b5a4093a70f3 Mon Sep 17 00:00:00 2001 From: Ghost Date: Thu, 1 Jan 2026 13:48:05 -0600 Subject: [PATCH 4/8] Update tests --- .github/workflows/tests.yml | 6 ++++ pyproject.toml | 3 +- tests/test_toc.py | 65 ++++++++++++++++++++++++++++++++++++- 3 files changed, 72 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index a643752..d49466b 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -38,5 +38,11 @@ jobs: - name: Format with ruff run: uv run ruff format --check + - name: Clone Blizzard UI + run: | + git clone https://github.com/Gethe/wow-ui-source.git + cd wow-ui-source + git checkout beta + - name: Test with pytest run: uv run pytest diff --git a/pyproject.toml b/pyproject.toml index c4ee203..2d99ba8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -59,7 +59,8 @@ exclude = [ "dist", "site-packages", "venv", - "pyproject.toml" + "pyproject.toml", + "wow-ui-source" ] line-length = 160 indent-width = 4 diff --git a/tests/test_toc.py b/tests/test_toc.py index 8972ff4..66a5a69 100644 --- a/tests/test_toc.py +++ b/tests/test_toc.py @@ -1,7 +1,9 @@ import os import pytest -from pytoc import TOCFile, TOCDependency +from pathlib import Path + +from pytoc import * PWD = os.path.dirname(os.path.realpath(__file__)) @@ -148,3 +150,64 @@ def test_read_export(): assert toc.OnlyBetaAndPTR == True assert toc.DefaultState == True assert toc.get_raw_files() == ["file1.lua", "file2.xml"] + + +def discover_toc_files(path: Path) -> list[Path]: + toc_files = [] + for root, _, files in path.walk(): + for file in files: + if file.endswith(".toc"): + toc_path = Path(root) / file + toc_files.append(toc_path) + + return toc_files + + +def test_blizzard_ui_conformance(): + ui_source_path = Path("wow-ui-source") / "Interface" + if not ui_source_path.exists(): + ui_source_path = Path(os.getenv("WOW_UI_SOURCE_PATH")) / "Interface" + + if not ui_source_path.exists(): + return + + failures = [] + + # collect all the toc files + toc_files = discover_toc_files(ui_source_path) + for file in toc_files: + try: + TOCFile(file) + except Exception as e: + failures.append((file, e)) + + assert not failures, failures + + +def test_addon_load_conditions(): + ctx = TOCEvaluationContext(TOCGameType.Mainline, TOCEnvironment.Global, TOCTextLocale.enUS) + + toc = TOCFile() + + toc.AllowLoadGameType = TOCAllowLoadGameType({TOCGameType.Wowhack}) + can_load, err = toc.can_load_addon(ctx) + assert (not can_load) and (err == TOCAddonLoadError.WrongGameType), err.name + + toc.AllowLoad = TOCAllowLoad({TOCEnvironment.Both}) + toc.AllowLoadEnvironment = TOCAllowLoadEnvironment({TOCEnvironment.Global}) + toc.AllowLoadGameType = TOCAllowLoadGameType({TOCGameType.Mainline}) + toc.AllowLoadTextLocale = TOCAllowLoadTextLocale({TOCTextLocale.enUS}) + + can_load, err = toc.can_load_addon(ctx) + assert can_load, err.name + + dep_name = "Blackjack" + dep_required = True + toc.add_dependency(dep_name, dep_required) + + can_load, err = toc.can_load_addon(ctx) + assert (not can_load) and (err == TOCAddonLoadError.MissingDependency), err.name + + ctx.load_addon(dep_name) + can_load, err = toc.can_load_addon(ctx) + assert can_load, err.name From cc7d358bba058713dc3ec6ea486231e65c44e895 Mon Sep 17 00:00:00 2001 From: Ghost Date: Thu, 1 Jan 2026 13:50:29 -0600 Subject: [PATCH 5/8] Update test_toc.py --- tests/test_toc.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/test_toc.py b/tests/test_toc.py index 66a5a69..4aae867 100644 --- a/tests/test_toc.py +++ b/tests/test_toc.py @@ -168,8 +168,7 @@ def test_blizzard_ui_conformance(): if not ui_source_path.exists(): ui_source_path = Path(os.getenv("WOW_UI_SOURCE_PATH")) / "Interface" - if not ui_source_path.exists(): - return + assert ui_source_path.exists(), "Unable to find UI source path" failures = [] From 21ac905a10d74afaa8ab9fc5886a24d75345e699 Mon Sep 17 00:00:00 2001 From: Ghost Date: Thu, 1 Jan 2026 14:15:52 -0600 Subject: [PATCH 6/8] Cleanup tests some more --- src/pytoc/context.py | 4 +--- tests/test_toc.py | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/src/pytoc/context.py b/src/pytoc/context.py index 29e23ea..65aa92b 100644 --- a/src/pytoc/context.py +++ b/src/pytoc/context.py @@ -21,10 +21,8 @@ def load_addon(self, addon_name: str): self.LoadedAddons[addon_name] = True def unload_addon(self, addon_name: str): - try: + if addon_name in self.LoadedAddons: self.LoadedAddons.pop(addon_name) - except KeyError: - ... def is_addon_loaded(self, addon_name: str) -> bool: return self.LoadedAddons.get(addon_name, False) diff --git a/tests/test_toc.py b/tests/test_toc.py index 4aae867..574a154 100644 --- a/tests/test_toc.py +++ b/tests/test_toc.py @@ -210,3 +210,37 @@ def test_addon_load_conditions(): ctx.load_addon(dep_name) can_load, err = toc.can_load_addon(ctx) assert can_load, err.name + + +def test_plain_file_entry(): + ctx = TOCEvaluationContext(TOCGameType.Mainline, TOCEnvironment.Global, TOCTextLocale.enUS) + + path = "Dragon/Dragon.lua" + file = TOCFileEntry(path) + assert str(file) == path + assert file.resolve_path(ctx) == path + assert file.should_load(ctx) + assert file.export() == path + + +def test_variable_file_entry(): + ctx = TOCEvaluationContext(TOCGameType.Mainline, TOCEnvironment.Global, TOCTextLocale.enUS) + + path = "[Family]/Dragon.lua" + file = TOCFileEntry(path) + assert str(file) == path + assert file.resolve_path(ctx) == f"{TOCFamily.Mainline}/Dragon.lua" + assert file.should_load(ctx) + assert file.export() == path + + +def test_conditional_file_entry(): + ctx = TOCEvaluationContext(TOCGameType.Wowhack, TOCEnvironment.Global, TOCTextLocale.enUS) + + path = "[Family]/Dragon.lua" + conditions = [TOCAllowLoadGameType({TOCGameType.Plunderstorm}), TOCAllowLoadEnvironment({TOCEnvironment.Both})] + file = TOCFileEntry(path, conditions) + assert str(file) == path + assert file.resolve_path(ctx) == f"{TOCFamily.Mainline}/Dragon.lua" + assert not file.should_load(ctx) + assert file.export() == "[Family]/Dragon.lua [AllowLoadGameType plunderstorm] [AllowLoadEnvironment Both]" From e859a3328a14c194882e530bdea5dacb954c5083 Mon Sep 17 00:00:00 2001 From: Ghost Date: Thu, 1 Jan 2026 14:21:44 -0600 Subject: [PATCH 7/8] 91%? adequate. --- tests/test_toc.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tests/test_toc.py b/tests/test_toc.py index 574a154..f934a29 100644 --- a/tests/test_toc.py +++ b/tests/test_toc.py @@ -244,3 +244,28 @@ def test_conditional_file_entry(): assert file.resolve_path(ctx) == f"{TOCFamily.Mainline}/Dragon.lua" assert not file.should_load(ctx) assert file.export() == "[Family]/Dragon.lua [AllowLoadGameType plunderstorm] [AllowLoadEnvironment Both]" + + +def test_textlocale_file_entry(): + ctx = TOCEvaluationContext(TOCGameType.Mists, TOCEnvironment.Global, TOCTextLocale.enUS) + + path = "[TextLocale]/Dragon.lua" + file = TOCFileEntry(path) + assert str(file) == path + assert file.resolve_path(ctx) == f"{TOCTextLocale.enUS}/Dragon.lua" + assert file.should_load(ctx) + assert file.export() == path + + +def test_addon_loading(): + ctx = TOCEvaluationContext(TOCGameType.Wowhack, TOCEnvironment.Global, TOCTextLocale.enUS) + assert ctx.LoadedAddons == {} + + addon_name = "Dragon" + assert not ctx.is_addon_loaded(addon_name) + + ctx.load_addon(addon_name) + assert ctx.is_addon_loaded(addon_name) + + ctx.unload_addon(addon_name) + assert not ctx.is_addon_loaded(addon_name) From 214ef94763df6eadd9b5a95a1621f72df94bdcd3 Mon Sep 17 00:00:00 2001 From: Ghost Date: Thu, 1 Jan 2026 14:23:25 -0600 Subject: [PATCH 8/8] One more text, and some organization --- tests/test_toc.py | 36 ++++++++++++++++++++++-------------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/tests/test_toc.py b/tests/test_toc.py index f934a29..52e35f9 100644 --- a/tests/test_toc.py +++ b/tests/test_toc.py @@ -152,6 +152,28 @@ def test_read_export(): assert toc.get_raw_files() == ["file1.lua", "file2.xml"] +def test_addon_loading(): + ctx = TOCEvaluationContext(TOCGameType.Wowhack, TOCEnvironment.Global, TOCTextLocale.enUS) + assert ctx.LoadedAddons == {} + + addon_name = "Dragon" + assert not ctx.is_addon_loaded(addon_name) + + ctx.load_addon(addon_name) + assert ctx.is_addon_loaded(addon_name) + + ctx.unload_addon(addon_name) + assert not ctx.is_addon_loaded(addon_name) + + +def test_ctx_family(): + ctx = TOCEvaluationContext(TOCGameType.Wowhack, TOCEnvironment.Global, TOCTextLocale.enUS) + assert ctx.Family == TOCFamily.Mainline + + ctx.GameType = TOCGameType.Mists + assert ctx.Family == TOCFamily.Classic + + def discover_toc_files(path: Path) -> list[Path]: toc_files = [] for root, _, files in path.walk(): @@ -255,17 +277,3 @@ def test_textlocale_file_entry(): assert file.resolve_path(ctx) == f"{TOCTextLocale.enUS}/Dragon.lua" assert file.should_load(ctx) assert file.export() == path - - -def test_addon_loading(): - ctx = TOCEvaluationContext(TOCGameType.Wowhack, TOCEnvironment.Global, TOCTextLocale.enUS) - assert ctx.LoadedAddons == {} - - addon_name = "Dragon" - assert not ctx.is_addon_loaded(addon_name) - - ctx.load_addon(addon_name) - assert ctx.is_addon_loaded(addon_name) - - ctx.unload_addon(addon_name) - assert not ctx.is_addon_loaded(addon_name)