Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,8 @@ exclude = [
"dist",
"site-packages",
"venv",
"pyproject.toml"
"pyproject.toml",
"wow-ui-source"
]
line-length = 160
indent-width = 4
Expand Down
28 changes: 28 additions & 0 deletions src/pytoc/context.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
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):
if addon_name in self.LoadedAddons:
self.LoadedAddons.pop(addon_name)

def is_addon_loaded(self, addon_name: str) -> bool:
return self.LoadedAddons.get(addon_name, False)
10 changes: 9 additions & 1 deletion src/pytoc/enums.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from enum import StrEnum
from enum import StrEnum, Enum


class TOCGameType(StrEnum):
Expand Down Expand Up @@ -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
18 changes: 1 addition & 17 deletions src/pytoc/file_entry.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
39 changes: 33 additions & 6 deletions src/pytoc/toc.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"\[([^\]]+)\]")

Expand Down Expand Up @@ -48,12 +48,13 @@ def StringToBoolean(string: str, defaultReturn: bool = False):


@dataclass
class Dependency:
class TOCDependency:
Name: str
Required: bool


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
Expand All @@ -77,7 +78,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
Expand All @@ -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:
Expand All @@ -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

Expand Down Expand Up @@ -234,9 +239,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<directive>` attribute
Expand Down Expand Up @@ -336,3 +341,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
133 changes: 131 additions & 2 deletions tests/test_toc.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import os
import pytest

from pytoc import TOCFile, Dependency
from pathlib import Path

from pytoc import *

PWD = os.path.dirname(os.path.realpath(__file__))

Expand Down Expand Up @@ -72,7 +74,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)

Expand Down Expand Up @@ -148,3 +150,130 @@ def test_read_export():
assert toc.OnlyBetaAndPTR == True
assert toc.DefaultState == True
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():
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"

assert ui_source_path.exists(), "Unable to find UI source path"

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


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]"


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