diff --git a/meta/common/__init__.py b/meta/common/__init__.py index 8e463f8..ee294f0 100644 --- a/meta/common/__init__.py +++ b/meta/common/__init__.py @@ -49,3 +49,22 @@ def replace_old_launchermeta_url(url): return o._replace(netloc="piston-meta.mojang.com").geturl() return url + + +def get_all_bases(cls, bases=None): + bases = bases or [] + bases.append(cls) + for c in cls.__bases__: + get_all_bases(c, bases) + return tuple(bases) + + +def merge_dict(base: dict, overlay: dict): + for k, v in base.items(): + if isinstance(v, dict): + merge_dict(v, overlay.setdefault(k, {})) + else: + if k not in overlay: + overlay[k] = v + + return overlay diff --git a/meta/model/__init__.py b/meta/model/__init__.py index b089040..caa0e6d 100644 --- a/meta/model/__init__.py +++ b/meta/model/__init__.py @@ -1,10 +1,11 @@ +import copy from datetime import datetime from typing import Optional, List, Dict, Any, Iterator import pydantic from pydantic import Field, validator -from ..common import serialize_datetime, replace_old_launchermeta_url +from ..common import serialize_datetime, replace_old_launchermeta_url, get_all_bases, merge_dict META_FORMAT_VERSION = 1 @@ -119,6 +120,40 @@ class MetaBase(pydantic.BaseModel): with open(file_path, "w") as f: f.write(self.json()) + def merge(self, other): + """ + Merge other object with self. + - Concatenates lists + - Combines sets + - Merges dictionaries (other takes priority) + - Recurses for all fields that are also MetaBase classes + - Overwrites for any other field type (int, string, ...) + """ + assert type(other) is type(self) + for key, field in self.__fields__.items(): + ours = getattr(self, key) + theirs = getattr(other, key) + if theirs is None: + continue + if ours is None: + setattr(self, key, theirs) + continue + + if isinstance(ours, list): + ours += theirs + elif isinstance(ours, set): + ours |= theirs + elif isinstance(ours, dict): + result = merge_dict(ours, copy.deepcopy(theirs)) + setattr(self, key, result) + elif MetaBase in get_all_bases(field.type_): + ours.merge(theirs) + else: + setattr(self, key, theirs) + + def __hash__(self): + return hash(self.json()) + class Config: allow_population_by_field_name = True