Skip to content

Commit

Permalink
(extensions.diff) breaking up __init__
Browse files Browse the repository at this point in the history
  • Loading branch information
snake-biscuits committed Jul 21, 2023
1 parent 4efb5d7 commit cbab31a
Show file tree
Hide file tree
Showing 5 changed files with 172 additions and 166 deletions.
163 changes: 0 additions & 163 deletions bsp_tool/extensions/diff/__init__.py
Original file line number Diff line number Diff line change
@@ -1,163 +0,0 @@
"""determining the equivialency of bsps & reporting findings in detail"""
import difflib
from typing import Any, Dict, Generator, List

from . import base
from . import shared
from .valve import source
# TODO: lightmap diffs

from bsp_tool import branches
from bsp_tool.base import Bsp
from bsp_tool.lumps import BasicBspLump, RawBspLump, ExternalRawBspLump


def diff_lumps(old_lump: Any, new_lump: Any) -> base.Diff:
"""lookup table & intialiser"""
LumpClasses = set()
for lump in (old_lump, new_lump):
if issubclass(lump.__class__, BasicBspLump):
LumpClasses.add(lump.LumpClass)
else: # SpecialLumpClass / RawBspLump
LumpClasses.add(lump.__class__)
# match LumpClasses to a base.Diff subclass
# TODO: mismatched lump type diffs (substitute defaults for alternate versions?)
# -- should only be used for extremely similar lumps
if len(LumpClasses) > 1:
# AbridgedDiff?
raise NotImplementedError("Cannot diff lumps of differring LumpClass")
if LumpClasses == {branches.shared.Entities}:
DiffClass = shared.EntitiesDiff
elif LumpClasses == {branches.valve.source.PakFile}:
DiffClass = source.PakFileDiff
elif RawBspLump in LumpClasses or ExternalRawBspLump in LumpClasses:
# TODO: core.xxd diff
raise NotImplementedError("Cannot diff raw lumps")
# if all([issubclass(lc, branches.base.BitField) for lc in LumpClasses]):
# DiffClass = base.BitFieldDiff
# if all([issubclass(lc, branches.base.MappedArray) for lc in LumpClasses]):
# DiffClass = base.MappedArrayDiff
# if all([issubclass(lc, branches.base.Struct) for lc in LumpClasses]):
# DiffClass = base.StructDiff
else: # default
DiffClass = base.Diff
return DiffClass(old_lump, new_lump)


class BspDiff:
"""deferred diffs of lumps & headers etc."""
# NOTE: not a base.Diff subclass
old: Bsp
new: Bsp

def __init__(self, old: Bsp, new: Bsp):
if old.branch != new.branch:
raise NotImplementedError("Cannot diff bsps from different branches")
self.old = old
self.new = new
self.headers = HeadersDiff(old.headers, new.headers)
# TODO: other metadata (file magic, version, revision, signature etc.)

def __getattr__(self, lump_name: str) -> Any:
"""retrieve differ for given lump"""
old_lump = getattr(self.old, lump_name, None)
new_lump = getattr(self.new, lump_name, None)
no_old_lump = old_lump is None
no_new_lump = new_lump is None
if no_old_lump and no_new_lump:
raise AttributeError(f"Neither bsp has {lump_name} lump to be diffed")
elif no_old_lump or no_new_lump:
return NoneDiff(old_lump, new_lump)
else:
diff = diff_lumps(old_lump, new_lump)
setattr(self, lump_name, diff) # cache
return diff

def has_no_changes(self) -> bool:
try:
assert self.headers.has_no_changes()
# TODO: other metadata
for lump in self.old.headers:
old_header = self.old.headers[lump]
new_header = self.new.headers[lump]
if old_header.length != 0 or new_header.length != 0:
assert getattr(self, lump).has_no_changes()
except AssertionError:
return False
return True

def what_changed(self) -> List[str]:
check = {"headers": self.headers.has_no_changes()}
# TODO: other metadata
for lump in self.old.headers:
old_header = self.old.headers[lump]
new_header = self.new.headers[lump]
if old_header.length != 0 or new_header.length != 0:
check[lump] = getattr(self, lump).has_no_changes()
return {attr for attr, unchanged in check.items() if not unchanged}

def save(self, base_filename: str, log_mode: base.LogMode = base.LogMode.VERBOSE):
"""generate & save .diff files"""
raise NotImplementedError()
# self.lump.unified_diff() -> individual files
# -- .bsp -> filename.00.ENTITIES.diff
# RespawnBsp format:
# -- .bsp -> filename.00XX.LUMP_NAME.diff
# -- .bsp_lump -> filename.external.00XX.LUMP_NAME.diff
# -- .ent -> filename.external.ent.xxxxx.diff
# should also generate a general / meta diff for metadata etc.


class NoneDiff(base.Diff):
"""for diffing against None"""
def short_stats(self) -> str:
brand_new = self.old is None
assert brand_new or self.new is None
if brand_new:
return f"{len(self.new)} insertions(+)"
else:
return f"{len(self.old)} deletions(-)"

def unified_diff(self) -> List[str]:
return [self.short_stats()]


class HeadersDiff(base.Diff):
# TODO: support comparisons between different branches
# TODO: how do we communicate a change in branch order?
# -- modern_warfare lump order & count is unique
# -- will probably need it's own class
old: Dict[str, Any]
new: Dict[str, Any]
_cache = Dict[str, List[str]]
# NOTE: changes on offset can be knock on affect of changes to an earlier lump

def __init__(self, old: Dict[str, Any], new: Dict[str, Any]):
super().__init__(old, new)
self._cache = dict()

def __getitem__(self, lump_name: str) -> str:
if lump_name not in {*self.old, *self.new}:
raise KeyError(f"No {lump_name} header to diff")
diff = self._cache.get(lump_name)
if diff is None:
old = f"{lump_name} {self.old[lump_name]!r}\n"
new = f"{lump_name} {self.new[lump_name]!r}\n"
diff = list(difflib.unified_diff([old], [new]))[3:]
self._cache[lump_name] = diff
return diff

# TODO: check headers in order
# -- sorted({(h.offset, h.length, i) for i, h in enumerate(self.old.headers.values()) if h.length > 0})
# -- find knock-on changes
# -- trivial differences (e.g. offset=0, length=0)

def short_stats(self) -> str:
# TODO: how to summarise?
# change in any attr but "offset"
raise NotImplementedError()

def unified_diff(self) -> Generator[str, None, None]:
for lump_name in self.old:
for line in self[lump_name]:
yield line
111 changes: 111 additions & 0 deletions bsp_tool/extensions/diff/bsps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import difflib
from typing import Any, Dict, List, Generator

from . import base

from bsp_tool.base import Bsp


class BspDiff:
"""deferred diffs of lumps & headers etc."""
# NOTE: not a base.Diff subclass
old: Bsp
new: Bsp

def __init__(self, old: Bsp, new: Bsp):
if old.branch != new.branch:
raise NotImplementedError("Cannot diff bsps from different branches")
self.old = old
self.new = new
self.headers = HeadersDiff(old.headers, new.headers)
# TODO: other metadata (file magic, version, revision, signature etc.)

def __getattr__(self, lump_name: str) -> Any:
"""retrieve differ for given lump"""
old_lump = getattr(self.old, lump_name, None)
new_lump = getattr(self.new, lump_name, None)
no_old_lump = old_lump is None
no_new_lump = new_lump is None
if no_old_lump and no_new_lump:
raise AttributeError(f"Neither bsp has {lump_name} lump to be diffed")
elif no_old_lump or no_new_lump:
return lumps.NoneDiff(old_lump, new_lump)
else:
diff = lumps.diff_lumps(old_lump, new_lump)
setattr(self, lump_name, diff) # cache
return diff

def has_no_changes(self) -> bool:
try:
assert self.headers.has_no_changes()
# TODO: other metadata
for lump in self.old.headers:
old_header = self.old.headers[lump]
new_header = self.new.headers[lump]
if old_header.length != 0 or new_header.length != 0:
assert getattr(self, lump).has_no_changes()
except AssertionError:
return False
return True

def what_changed(self) -> List[str]:
check = {"headers": self.headers.has_no_changes()}
# TODO: other metadata
for lump in self.old.headers:
old_header = self.old.headers[lump]
new_header = self.new.headers[lump]
if old_header.length != 0 or new_header.length != 0:
check[lump] = getattr(self, lump).has_no_changes()
return {attr for attr, unchanged in check.items() if not unchanged}

def save(self, base_filename: str, log_mode: base.LogMode = base.LogMode.VERBOSE):
"""generate & save .diff files"""
raise NotImplementedError()
# self.lump.unified_diff() -> individual files
# -- .bsp -> filename.00.ENTITIES.diff
# RespawnBsp format:
# -- .bsp -> filename.00XX.LUMP_NAME.diff
# -- .bsp_lump -> filename.external.00XX.LUMP_NAME.diff
# -- .ent -> filename.external.ent.xxxxx.diff
# should also generate a general / meta diff for metadata etc.


class HeadersDiff(base.Diff):
# TODO: support comparisons between different branches
# TODO: how do we communicate a change in branch order?
# -- modern_warfare lump order & count is unique
# -- will probably need it's own class
old: Dict[str, Any]
new: Dict[str, Any]
_cache = Dict[str, List[str]]
# NOTE: changes on offset can be knock on affect of changes to an earlier lump

def __init__(self, old: Dict[str, Any], new: Dict[str, Any]):
super().__init__(old, new)
self._cache = dict()

def __getitem__(self, lump_name: str) -> str:
if lump_name not in {*self.old, *self.new}:
raise KeyError(f"No {lump_name} header to diff")
diff = self._cache.get(lump_name)
if diff is None:
old = f"{lump_name} {self.old[lump_name]!r}\n"
new = f"{lump_name} {self.new[lump_name]!r}\n"
diff = list(difflib.unified_diff([old], [new]))[3:]
self._cache[lump_name] = diff
return diff

# TODO: check headers in order
# -- sorted({(h.offset, h.length, i) for i, h in enumerate(self.old.headers.values()) if h.length > 0})
# -- find knock-on changes
# -- trivial differences (e.g. offset=0, length=0)

def short_stats(self) -> str:
# TODO: how to summarise?
# change in any attr but "offset"
raise NotImplementedError()

def unified_diff(self) -> Generator[str, None, None]:
for lump_name in self.old:
for line in self[lump_name]:
yield line
4 changes: 4 additions & 0 deletions bsp_tool/extensions/diff/lightmaps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# from .. import lightmaps
# TODO

...
54 changes: 54 additions & 0 deletions bsp_tool/extensions/diff/lumps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
from typing import Any, List

from . import base
from . import shared
from .valve import source

from bsp_tool import branches
from bsp_tool.lumps import BasicBspLump, RawBspLump, ExternalRawBspLump


def diff_lumps(old_lump: Any, new_lump: Any) -> base.Diff:
"""lookup table & intialiser"""
LumpClasses = set()
for lump in (old_lump, new_lump):
if issubclass(lump.__class__, BasicBspLump):
LumpClasses.add(lump.LumpClass)
else: # SpecialLumpClass / RawBspLump
LumpClasses.add(lump.__class__)
# match LumpClasses to a base.Diff subclass
# TODO: mismatched lump type diffs (substitute defaults for alternate versions?)
# -- should only be used for extremely similar lumps
if len(LumpClasses) > 1:
# AbridgedDiff?
raise NotImplementedError("Cannot diff lumps of differring LumpClass")
if LumpClasses == {branches.shared.Entities}:
DiffClass = shared.EntitiesDiff
elif LumpClasses == {branches.valve.source.PakFile}:
DiffClass = source.PakFileDiff
elif RawBspLump in LumpClasses or ExternalRawBspLump in LumpClasses:
# TODO: core.xxd diff
raise NotImplementedError("Cannot diff raw lumps")
# if all([issubclass(lc, branches.base.BitField) for lc in LumpClasses]):
# DiffClass = base.BitFieldDiff
# if all([issubclass(lc, branches.base.MappedArray) for lc in LumpClasses]):
# DiffClass = base.MappedArrayDiff
# if all([issubclass(lc, branches.base.Struct) for lc in LumpClasses]):
# DiffClass = base.StructDiff
else: # default
DiffClass = base.Diff
return DiffClass(old_lump, new_lump)


class NoneDiff(base.Diff):
"""for diffing against None"""
def short_stats(self) -> str:
brand_new = self.old is None
assert brand_new or self.new is None
if brand_new:
return f"{len(self.new)} insertions(+)"
else:
return f"{len(self.old)} deletions(-)"

def unified_diff(self) -> List[str]:
return [self.short_stats()]
6 changes: 3 additions & 3 deletions tests/test_save.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
from bsp_tool.branches.strata import strata
from bsp_tool.branches.valve import orange_box
# extensions
from bsp_tool.extensions import diff
import bsp_tool.extensions.diff.bsps as diff_bsps

import pytest

Expand Down Expand Up @@ -96,7 +96,7 @@ def save_and_diff_backup(BspClass: object, branch_script: ModuleType, map_path:
filename_bak_ext = f"{filename}.bak{ext}"
old_bsp = BspClass(branch_script, filename_bak_ext) # original file
new_bsp = BspClass(branch_script, filename_ext) # saved copy
return diff.BspDiff(old_bsp, new_bsp)
return diff_bsps.BspDiff(old_bsp, new_bsp)


# tests
Expand All @@ -112,7 +112,7 @@ def save_and_diff_backup(BspClass: object, branch_script: ModuleType, map_path:
@pytest.mark.xfail(raises=NotImplementedError, reason="not implemented yet")
@map_dirs_to_test("Call of Duty 4", "Call of Duty 4/mp", ext="*.d3dbsp")
def test_D3DBsp_modern_warfare(map_path: str):
# TODO: diff.HeadersDiff isn't ready for modern_warfare
# TODO: diff.bsps.HeadersDiff isn't ready for modern_warfare
# bsp_diff = save_and_diff_backup(D3DBsp, modern_warfare, map_path)
raise NotImplementedError()
...
Expand Down

0 comments on commit cbab31a

Please sign in to comment.