Skip to content

Commit

Permalink
Separate TocReader class, load multiple interfaces
Browse files Browse the repository at this point in the history
  • Loading branch information
layday committed Apr 7, 2024
1 parent 473d62d commit 84be707
Show file tree
Hide file tree
Showing 7 changed files with 118 additions and 75 deletions.
24 changes: 14 additions & 10 deletions src/instawow/_sources/github.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@
from ..catalogue.cataloguer import AddonKey, CatalogueEntry
from ..definitions import ChangelogFormat, Defn, SourceMetadata, Strategy
from ..http import CACHE_INDEFINITELY, ClientSessionType
from ..matchers.addon_toc import TocReader
from ..pkg_archives import find_archive_addon_tocs
from ..resolvers import BaseResolver, HeadersIntent, PkgCandidate
from ..utils import (
StrEnum,
TocReader,
as_plain_text_data_url,
extract_byte_range_offset,
)
Expand Down Expand Up @@ -263,11 +263,15 @@ async def __find_match_from_zip_contents(self, assets: list[_GithubRelease_Asset
toc_file_text = dynamic_addon_zip.read(main_toc_filename).decode('utf-8-sig')
toc_reader = TocReader(toc_file_text)

interface_version = toc_reader['Interface']
logger.debug(f'found interface version {interface_version!r} in {main_toc_filename}')
if interface_version and self._manager_ctx.config.game_flavour.to_flavour_keyed_enum(
logger.debug(
f'found interface versions {toc_reader.interfaces!r} in {main_toc_filename}'
)
desired_version_range = self._manager_ctx.config.game_flavour.to_flavour_keyed_enum(
FlavourVersionRange
).contains(int(interface_version)):
)
if toc_reader.interfaces and any(
desired_version_range.contains(i) for i in toc_reader.interfaces
):
matching_asset = candidate
break

Expand All @@ -293,10 +297,10 @@ async def __find_match_from_release_json(
if not releases:
return None

wanted_release_json_flavor = self._manager_ctx.config.game_flavour.to_flavour_keyed_enum(
desired_release_json_flavor = self._manager_ctx.config.game_flavour.to_flavour_keyed_enum(
_PackagerReleaseJsonFlavor
)
wanted_version_range = self._manager_ctx.config.game_flavour.to_flavour_keyed_enum(
desired_version_range = self._manager_ctx.config.game_flavour.to_flavour_keyed_enum(
FlavourVersionRange
)

Expand All @@ -305,13 +309,13 @@ def is_compatible_release(release: _PackagerReleaseJson_Release):
return False

for metadata in release['metadata']:
if metadata['flavor'] != wanted_release_json_flavor:
if metadata['flavor'] != desired_release_json_flavor:
continue

interface_version = metadata['interface']
if not wanted_version_range.contains(interface_version):
if not desired_version_range.contains(interface_version):
logger.info(
f'interface number "{interface_version}" and flavor "{wanted_release_json_flavor}" mismatch'
f'interface number "{interface_version}" and flavor "{desired_release_json_flavor}" mismatch'
)
continue

Expand Down
9 changes: 6 additions & 3 deletions src/instawow/matchers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
from ..catalogue import synchronise as synchronise_catalogue
from ..definitions import Defn
from ..utils import (
TocReader,
bucketise,
fauxfrozen,
gather,
Expand All @@ -24,6 +23,7 @@
)
from ..wow_installations import Flavour
from ._addon_hashing import generate_wowup_addon_hash
from .addon_toc import TocReader


class Matcher(Protocol): # pragma: no cover
Expand Down Expand Up @@ -81,11 +81,14 @@ def hash_contents(self, __method: AddonHashMethod) -> str:
return generate_wowup_addon_hash(self.path)

def get_defns_from_toc_keys(self, keys_and_ids: Iterable[tuple[str, str]]) -> frozenset[Defn]:
return frozenset(Defn(s, i) for k, s in keys_and_ids for i in (self.toc_reader[k],) if i)
return frozenset(
Defn(s, i) for k, s in keys_and_ids for i in (self.toc_reader.get(k),) if i
)

@cached_property
def version(self) -> str:
return self.toc_reader['Version', 'X-Packaged-Version', 'X-Curse-Packaged-Version'] or ''
version_keys = ('Version', 'X-Packaged-Version', 'X-Curse-Packaged-Version')
return next(filter(None, map(self.toc_reader.get, version_keys)), '')


def _get_unreconciled_folders(manager_ctx: manager_ctx.ManagerCtx):
Expand Down
38 changes: 38 additions & 0 deletions src/instawow/matchers/addon_toc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
from __future__ import annotations

from collections.abc import Iterator, Mapping
from functools import cached_property
from pathlib import Path

from typing_extensions import Self


class TocReader(Mapping[str, str]):
"""Extracts key-value pairs from TOC files."""

def __init__(self, contents: str) -> None:
self._entries = {
k: v
for e in contents.splitlines()
if e.startswith('##')
for k, v in (map(str.strip, e.lstrip('#').partition(':')[::2]),)
if k
}

def __iter__(self) -> Iterator[str]:
return iter(self._entries)

def __getitem__(self, key: str, /) -> str:
return self._entries[key]

def __len__(self) -> int:
return len(self._entries)

@classmethod
def from_path(cls, path: Path) -> Self:
return cls(path.read_text(encoding='utf-8-sig', errors='replace'))

@cached_property
def interfaces(self) -> list[int]:
interface = self.get('Interface')
return list(map(int, interface.split(','))) if interface else []
24 changes: 1 addition & 23 deletions src/instawow/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
Iterable,
Iterator,
Sequence,
Set,
)
from contextlib import contextmanager
from functools import partial, wraps
Expand Down Expand Up @@ -54,29 +55,6 @@ def read_resource_as_text(package: ModuleType, resource: str, encoding: str = 'u
return importlib.resources.files(package).joinpath(resource).read_text(encoding)


class TocReader:
"""Extracts key-value pairs from TOC files."""

def __init__(self, contents: str) -> None:
self.entries = {
k: v
for e in contents.splitlines()
if e.startswith('##')
for k, v in (map(str.strip, e.lstrip('#').partition(':')[::2]),)
if k
}

def __getitem__(self, key: str | tuple[str, ...]) -> str | None:
if isinstance(key, tuple):
return next(filter(None, map(self.entries.get, key)), None)
else:
return self.entries.get(key)

@classmethod
def from_path(cls, path: Path) -> TocReader:
return cls(path.read_text(encoding='utf-8-sig', errors='replace'))


def fill(it: Iterable[_T], fill: _T, length: int) -> Iterable[_T]:
"Fill an iterable of specified length."
return islice(chain(it, repeat(fill)), 0, length)
Expand Down
6 changes: 2 additions & 4 deletions tests/fixtures/http/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@

from __future__ import annotations

import importlib.resources
import json
import re
from collections.abc import Awaitable, Callable
from functools import cache
from io import BytesIO
from pathlib import Path
from typing import Any
from zipfile import ZipFile

Expand All @@ -17,13 +17,11 @@

from instawow import __version__

_HERE = Path(__file__).parent

_match_any = re.compile(r'.*')


def _load_fixture(filename: str):
return (_HERE / filename).read_bytes()
return (importlib.resources.files() / filename).read_bytes()


def _load_json_fixture(filename: str):
Expand Down
50 changes: 50 additions & 0 deletions tests/test_addon_toc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
from __future__ import annotations

import importlib.resources
from pathlib import Path

import pytest

from instawow.matchers.addon_toc import TocReader


@pytest.fixture
def fake_addon_toc():
with importlib.resources.as_file(
importlib.resources.files() / 'fixtures' / 'FakeAddon' / 'FakeAddon.toc'
) as file:
yield file


def test_loading_toc_from_path(fake_addon_toc: Path):
TocReader.from_path(fake_addon_toc)
with pytest.raises(FileNotFoundError):
TocReader.from_path(fake_addon_toc.parent / 'MissingAddon.toc')


def test_parsing_toc_entries(fake_addon_toc: Path):
toc_reader = TocReader.from_path(fake_addon_toc)
assert dict(toc_reader) == {
'Normal': 'Normal entry',
'Compact': 'Compact entry',
}


def test_toc_entry_indexing(fake_addon_toc: Path):
toc_reader = TocReader.from_path(fake_addon_toc)
assert toc_reader['Normal'] == 'Normal entry'
assert toc_reader['Compact'] == 'Compact entry'
assert toc_reader.get('Indented') is None
assert toc_reader.get('Comment') is None
assert toc_reader.get('Nonexistent') is None


def test_toc_interfaces():
toc_reader = TocReader('## Interface: 10100')
assert toc_reader.interfaces == [10100]

toc_reader = TocReader('## Interface: 10100, 20200')
assert toc_reader.interfaces == [10100, 20200]

toc_reader = TocReader('## Interface: 10100 , 20200 ')
assert toc_reader.interfaces == [10100, 20200]
42 changes: 7 additions & 35 deletions tests/test_utils.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
from __future__ import annotations

import asyncio
import importlib.resources
import sys
import time
from pathlib import Path

import pytest

from instawow.matchers.addon_toc import TocReader
from instawow.utils import (
TocReader,
bucketise,
file_uri_to_path,
merge_intersecting_sets,
Expand All @@ -19,39 +20,10 @@

@pytest.fixture
def fake_addon_toc():
return Path(__file__).parent / 'fixtures' / 'FakeAddon' / 'FakeAddon.toc'


def test_loading_toc_from_path(fake_addon_toc: Path):
TocReader.from_path(fake_addon_toc)
with pytest.raises(FileNotFoundError):
TocReader.from_path(fake_addon_toc.parent / 'MissingAddon.toc')


def test_parsing_toc_entries(fake_addon_toc: Path):
toc_reader = TocReader.from_path(fake_addon_toc)
assert toc_reader.entries == {
'Normal': 'Normal entry',
'Compact': 'Compact entry',
}


def test_toc_entry_indexing(fake_addon_toc: Path):
toc_reader = TocReader.from_path(fake_addon_toc)
assert toc_reader['Normal'] == 'Normal entry'
assert toc_reader['Compact'] == 'Compact entry'
assert toc_reader['Indented'] is None
assert toc_reader['Comment'] is None
assert toc_reader['Nonexistent'] is None


def test_toc_entry_multiindexing(fake_addon_toc: Path):
toc_reader = TocReader.from_path(fake_addon_toc)
assert toc_reader['Normal', 'Compact'] == 'Normal entry'
assert toc_reader['Compact', 'Normal'] == 'Compact entry'
assert toc_reader['Indented', 'Normal'] == 'Normal entry'
assert toc_reader['Nonexistent', 'Indented'] is None
assert toc_reader['Nonexistent', 'Indented', 'Normal'] == 'Normal entry'
with importlib.resources.as_file(
importlib.resources.files() / 'fixtures' / 'FakeAddon' / 'FakeAddon.toc'
) as file:
yield file


def test_bucketise_bucketises_by_putting_things_in_a_bucketing_bucket():
Expand All @@ -60,7 +32,7 @@ def test_bucketise_bucketises_by_putting_things_in_a_bucketing_bucket():

def test_tabulate_spits_out_ascii_table(fake_addon_toc: Path):
toc_reader = TocReader.from_path(fake_addon_toc)
data = [('key', 'value'), *toc_reader.entries.items()]
data = [('key', 'value'), *toc_reader.items()]
assert tabulate(data) == (
' key value \n'
'------- -------------\n'
Expand Down

0 comments on commit 84be707

Please sign in to comment.