Skip to content

Commit

Permalink
work on gff comparison logic
Browse files Browse the repository at this point in the history
  • Loading branch information
th3w1zard1 committed Mar 22, 2024
1 parent 1bee009 commit 1caaf9c
Show file tree
Hide file tree
Showing 3 changed files with 169 additions and 13 deletions.
88 changes: 75 additions & 13 deletions Libraries/PyKotor/src/pykotor/resource/formats/gff/gff_data.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

import difflib
import math
import os

Expand Down Expand Up @@ -155,6 +156,78 @@ def return_type(
raise ValueError(self)


class Difference:
def __init__(self, path: os.PathLike | str, old_value: Any, new_value: Any):
"""Initializes a Difference instance representing a specific difference between two GFFStructs.
Args:
----
path (os.PathLike | str): The path to the value within the GFFStruct where the difference was found.
old_value (Any): The value from the original GFFStruct at the specified path.
new_value (Any): The value from the compared GFFStruct at the specified path.
"""
self.path: PureWindowsPath = PureWindowsPath.pathify(path)
self.old_value: Any = old_value
self.new_value: Any = new_value

def __repr__(self):
return f"Difference(path={self.path}, old_value={self.old_value}, new_value={self.new_value})"

class GFFCompareResult:
"""A comparison result from gff.compare/GFFStruct.compare.
Contains enough differential information between the two GFF structs that it can be used to take one gff and reconstruct the other.
Helper methods also exist for working with the data in other code.
Backwards-compatibility note: the original gff.compare used to return a simple boolean. True if the gffs were the same, False if not. This class
attempts to keep backwards compatibility while ensuring we can still return a type that's more detailed and informative.
"""
def __init__(self):
self.differences: list[Difference] = []

def __bool__(self):
# Return False if the list has any contents (meaning the objects are different), True if it's empty.
return not bool(self.differences)

def add_difference(self, path, old_value, new_value):
"""Adds a difference to the collection of tracked differences.
Args:
----
path (str): The path to the value where the difference was found.
old_value (Any): The original value at the specified path.
new_value (Any): The new value at the specified path that differs from the original.
"""
self.differences.append(Difference(path, old_value, new_value))

def get_changed_values(self) -> list[Difference]:
"""Returns a list of differences where the value has changed from the original.
Returns:
-------
list[Difference]: The list of differences with changed values.
"""
return [diff for diff in self.differences if diff.old_value is not None and diff.new_value is not None and diff.old_value != diff.new_value]

def get_new_values(self) -> list[Difference]:
"""Returns a list of differences where a new value is present in the compared GFFStruct.
Returns:
-------
list[Difference]: The list of differences with new values.
"""
return [diff for diff in self.differences if diff.old_value is None and diff.new_value is not None]

def get_removed_values(self) -> list[Difference]:
"""Returns a list of differences where a value is present in the original GFFStruct but not in the compared.
Returns:
-------
list[Difference]: The list of differences with removed values.
"""
return [diff for diff in self.differences if diff.old_value is not None and diff.new_value is None]


class GFF:
"""Represents the data of a GFF file."""

Expand Down Expand Up @@ -461,19 +534,8 @@ def is_ignorable_comparison(
continue

formatted_old_value, formatted_new_value = map(str, (old_value, new_value))
newlines_in_old: int = formatted_old_value.count("\n")
newlines_in_new: int = formatted_new_value.count("\n")

if newlines_in_old > 1 or newlines_in_new > 1:
formatted_old_value, formatted_new_value = compare_and_format(formatted_old_value, formatted_new_value)
log_func(f"Field '{old_ftype.name}' is different at '{child_path}': {format_text(formatted_old_value)}<-vvv->{format_text(formatted_new_value)}")
continue

if newlines_in_old == 1 or newlines_in_new == 1:
log_func(f"Field '{old_ftype.name}' is different at '{child_path}': {os.linesep}{formatted_old_value}{os.linesep}<-vvv->{os.linesep}{formatted_new_value}")
continue

log_func(f"Field '{old_ftype.name}' is different at '{child_path}': {formatted_old_value} --> {formatted_new_value}")
diff = difflib.ndiff(formatted_old_value.splitlines(keepends=True), formatted_new_value.splitlines(keepends=True))
log_func("\n".join(diff))

return is_same

Expand Down
Empty file.
94 changes: 94 additions & 0 deletions Libraries/PyKotor/src/pykotor/tslpatcher/diff/gff.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
from __future__ import annotations


def flatten_differences(compare_result):
"""Flattens the differences from GFFCompareResult into a flat dictionary.
Args:
----
compare_result (GFFCompareResult): The result of a GFF comparison.
Returns:
-------
dict: A flat dictionary representing the changes.
"""
flat_changes = {}
for diff in compare_result.get_differences():
path_str = str(diff.path).replace("\\", "/") # Use forward slashes for INI compatibility
if diff.new_value is not None: # Changed or added
flat_changes[path_str] = diff.new_value
else: # Removed
flat_changes[path_str] = None # Represent removals as None or consider a special marker
return flat_changes

def build_hierarchy(flat_changes):
"""Builds a hierarchical structure suitable for INI serialization from flat changes.
Args:
----
flat_changes (dict): The flat dictionary of changes.
Returns:
-------
dict: A hierarchical dictionary representing the nested structure.
"""
hierarchy = {}
for path, value in flat_changes.items():
parts = path.split("/")
current_level = hierarchy
for part in parts[:-1]: # Navigate/create to the correct nested level, excluding the last part
if part not in current_level:
current_level[part] = {}
current_level = current_level[part]
current_level[parts[-1]] = value # Set the final part as the value
return hierarchy

def serialize_to_ini(hierarchy):
"""Serializes a hierarchical dictionary into an INI-formatted string.
Args:
hierarchy (dict): The hierarchical dictionary representing nested changes.
Returns:
str: A string formatted in INI structure.
"""
ini_lines = []
def serialize_section(name, content, indent_level=0):
"""Serializes a section of the hierarchy into INI format, recursively for nested sections.
Args:
name (str): The name of the section.
content (dict): The content of the section.
indent_level (int): The current indentation level (for nested sections).
"""
prefix = " " * indent_level * 4 # TODO(th3w1zard1): adjust indent later.
if indent_level == 0:
ini_lines.append(f"[{name}]")
else:
ini_lines.append(f"{prefix}{name}=")

for key, value in content.items():
if isinstance(value, dict):
# Nested section
serialize_section(key, value, indent_level + 1)
else:
# Key-value pair
if value is None:
value_str = "null" # TODO(th3w1zard1): determine nonexistence and use a singular value.
elif isinstance(value, str) and " " in value:
value_str = f'"{value}"' # Quote strings with spaces
else:
value_str = str(value)
ini_lines.append(f"{prefix}{key}={value_str}")

# Start serialization with the root level
for section_name, section_content in hierarchy.items():
serialize_section(section_name, section_content)

return "\n".join(ini_lines)

if __name__ == "__main__":
#gff_compare_result = something.compare(another)
hierarchy = build_hierarchy(flatten_differences(gff_compare_result))
ini_content = serialize_to_ini(hierarchy)
print(ini_content)

0 comments on commit 1caaf9c

Please sign in to comment.