diff options
Diffstat (limited to 'meta')
| -rw-r--r-- | meta/__init__.py | 1 | ||||
| -rw-r--r-- | meta/common.py | 19 | ||||
| -rw-r--r-- | meta/fabricutil.py | 32 | ||||
| -rw-r--r-- | meta/forgeutil.py | 323 | ||||
| -rw-r--r-- | meta/jsonobject/__init__.py | 17 | ||||
| -rw-r--r-- | meta/jsonobject/api.py | 53 | ||||
| -rw-r--r-- | meta/jsonobject/base.py | 394 | ||||
| -rw-r--r-- | meta/jsonobject/base_properties.py | 320 | ||||
| -rw-r--r-- | meta/jsonobject/containers.py | 252 | ||||
| -rw-r--r-- | meta/jsonobject/exceptions.py | 10 | ||||
| -rw-r--r-- | meta/jsonobject/properties.py | 155 | ||||
| -rw-r--r-- | meta/jsonobject/utils.py | 57 | ||||
| -rw-r--r-- | meta/liteloaderutil.py | 118 | ||||
| -rw-r--r-- | meta/metautil.py | 477 |
14 files changed, 2228 insertions, 0 deletions
diff --git a/meta/__init__.py b/meta/__init__.py new file mode 100644 index 0000000000..05cee3e7d7 --- /dev/null +++ b/meta/__init__.py @@ -0,0 +1 @@ +"""Meta package of meta""" diff --git a/meta/common.py b/meta/common.py new file mode 100644 index 0000000000..5454f517f3 --- /dev/null +++ b/meta/common.py @@ -0,0 +1,19 @@ +import os + + +def polymc_path(): + if "PMC_DIR" in os.environ: + return os.environ["PMC_DIR"] + return "polymc" + + +def upstream_path(): + if "UPSTREAM_DIR" in os.environ: + return os.environ["UPSTREAM_DIR"] + return "upstream" + + +def ensure_component_dir(component_id): + path = os.path.join(polymc_path(), component_id) + if not os.path.exists(path): + os.makedirs(path) diff --git a/meta/fabricutil.py b/meta/fabricutil.py new file mode 100644 index 0000000000..2f38f8f051 --- /dev/null +++ b/meta/fabricutil.py @@ -0,0 +1,32 @@ +from .metautil import * + + +class FabricInstallerArguments(JsonObject): + client = ListProperty(StringProperty) + common = ListProperty(StringProperty) + server = ListProperty(StringProperty) + + +class FabricInstallerLaunchwrapper(JsonObject): + tweakers = ObjectProperty(FabricInstallerArguments, required=True) + + +class FabricInstallerLibraries(JsonObject): + client = ListProperty(PolyMCLibrary) + common = ListProperty(PolyMCLibrary) + server = ListProperty(PolyMCLibrary) + + +class FabricInstallerDataV1(JsonObject): + version = IntegerProperty(required=True) + libraries = ObjectProperty(FabricInstallerLibraries, required=True) + mainClass = DefaultProperty() + arguments = ObjectProperty(FabricInstallerArguments, required=False) + launchwrapper = ObjectProperty(FabricInstallerLaunchwrapper, required=False) + + +class FabricJarInfo(JsonObject): + releaseTime = ISOTimestampProperty() + size = IntegerProperty() + sha256 = StringProperty() + sha1 = StringProperty() diff --git a/meta/forgeutil.py b/meta/forgeutil.py new file mode 100644 index 0000000000..4c2615cb8d --- /dev/null +++ b/meta/forgeutil.py @@ -0,0 +1,323 @@ +from collections import namedtuple + +from .metautil import * + + +# A post-processed entry constructed from the reconstructed Forge version index +class ForgeVersion: + def __init__(self, entry): + self.build = entry.build + self.rawVersion = entry.version + self.mcversion = entry.mcversion + self.mcversion_sane = self.mcversion.replace("_pre", "-pre", 1) + self.branch = entry.branch + self.installer_filename = None + self.installer_url = None + self.universal_filename = None + self.universal_url = None + self.changelog_url = None + self.longVersion = "%s-%s" % (self.mcversion, self.rawVersion) + if self.branch != None: + self.longVersion = self.longVersion + "-%s" % (self.branch) + for classifier, fileentry in entry.files.items(): + extension = fileentry.extension + checksum = fileentry.hash + filename = fileentry.filename(self.longVersion) + url = fileentry.url(self.longVersion) + if (classifier == "installer") and (extension == "jar"): + self.installer_filename = filename + self.installer_url = url + if (classifier == "universal" or classifier == "client") and (extension == "jar" or extension == "zip"): + self.universal_filename = filename + self.universal_url = url + if (classifier == "changelog") and (extension == "txt"): + self.changelog_url = url + + def name(self): + return "Forge %d" % (self.build) + + def usesInstaller(self): + if self.installer_url == None: + return False + if self.mcversion == "1.5.2": + return False + return True + + def filename(self): + if self.usesInstaller(): + return self.installer_filename + else: + return self.universal_filename + + def url(self): + if self.usesInstaller(): + return self.installer_url + else: + return self.universal_url + + def isSupported(self): + if self.url() == None: + return False + + versionElements = self.rawVersion.split('.') + if len(versionElements) < 1: + return False + + majorVersionStr = versionElements[0] + if not majorVersionStr.isnumeric(): + return False + + # majorVersion = int(majorVersionStr) + # if majorVersion >= 37: + # return False + + return True + + +class ForgeFile(JsonObject): + classifier = StringProperty(required=True) + hash = StringProperty(required=True) + extension = StringProperty(required=True) + + def filename(self, longversion): + return "%s-%s-%s.%s" % ("forge", longversion, self.classifier, self.extension) + + def url(self, longversion): + return "https://files.minecraftforge.net/maven/net/minecraftforge/forge/%s/%s" % ( + longversion, self.filename(longversion)) + + +class ForgeEntry(JsonObject): + longversion = StringProperty(required=True) + mcversion = StringProperty(required=True) + version = StringProperty(required=True) + build = IntegerProperty(required=True) + branch = StringProperty() + latest = BooleanProperty() + recommended = BooleanProperty() + files = DictProperty(ForgeFile) + + +class ForgeMcVersionInfo(JsonObject): + latest = StringProperty() + recommended = StringProperty() + versions = ListProperty(StringProperty()) + + +class DerivedForgeIndex(JsonObject): + versions = DictProperty(ForgeEntry) + by_mcversion = DictProperty(ForgeMcVersionInfo) + + +''' +FML library mappings - these are added to legacy Forge versions because Forge no longer can download these +by itself - the locations have changed and some of this has to be rehosted on PolyMC servers. +''' + +FMLLib = namedtuple('FMLLib', ('filename', 'checksum', 'ours')) + +fmlLibsMapping = {} + +fmlLibsMapping["1.3.2"] = [ + FMLLib("argo-2.25.jar", "bb672829fde76cb163004752b86b0484bd0a7f4b", False), + FMLLib("guava-12.0.1.jar", "b8e78b9af7bf45900e14c6f958486b6ca682195f", False), + FMLLib("asm-all-4.0.jar", "98308890597acb64047f7e896638e0d98753ae82", False) +] + +fml14 = [ + FMLLib("argo-2.25.jar", "bb672829fde76cb163004752b86b0484bd0a7f4b", False), + FMLLib("guava-12.0.1.jar", "b8e78b9af7bf45900e14c6f958486b6ca682195f", False), + FMLLib("asm-all-4.0.jar", "98308890597acb64047f7e896638e0d98753ae82", False), + FMLLib("bcprov-jdk15on-147.jar", "b6f5d9926b0afbde9f4dbe3db88c5247be7794bb", False) +] +fmlLibsMapping["1.4"] = fml14; +fmlLibsMapping["1.4.1"] = fml14; +fmlLibsMapping["1.4.2"] = fml14; +fmlLibsMapping["1.4.3"] = fml14; +fmlLibsMapping["1.4.4"] = fml14; +fmlLibsMapping["1.4.5"] = fml14; +fmlLibsMapping["1.4.6"] = fml14; +fmlLibsMapping["1.4.7"] = fml14; + +fmlLibsMapping["1.5"] = [ + FMLLib("argo-small-3.2.jar", "58912ea2858d168c50781f956fa5b59f0f7c6b51", False), + FMLLib("guava-14.0-rc3.jar", "931ae21fa8014c3ce686aaa621eae565fefb1a6a", False), + FMLLib("asm-all-4.1.jar", "054986e962b88d8660ae4566475658469595ef58", False), + FMLLib("bcprov-jdk15on-148.jar", "960dea7c9181ba0b17e8bab0c06a43f0a5f04e65", True), + FMLLib("deobfuscation_data_1.5.zip", "5f7c142d53776f16304c0bbe10542014abad6af8", False), + FMLLib("scala-library.jar", "458d046151ad179c85429ed7420ffb1eaf6ddf85", True) +] + +fmlLibsMapping["1.5.1"] = [ + FMLLib("argo-small-3.2.jar", "58912ea2858d168c50781f956fa5b59f0f7c6b51", False), + FMLLib("guava-14.0-rc3.jar", "931ae21fa8014c3ce686aaa621eae565fefb1a6a", False), + FMLLib("asm-all-4.1.jar", "054986e962b88d8660ae4566475658469595ef58", False), + FMLLib("bcprov-jdk15on-148.jar", "960dea7c9181ba0b17e8bab0c06a43f0a5f04e65", True), + FMLLib("deobfuscation_data_1.5.1.zip", "22e221a0d89516c1f721d6cab056a7e37471d0a6", False), + FMLLib("scala-library.jar", "458d046151ad179c85429ed7420ffb1eaf6ddf85", True) +] + +fmlLibsMapping["1.5.2"] = [ + FMLLib("argo-small-3.2.jar", "58912ea2858d168c50781f956fa5b59f0f7c6b51", False), + FMLLib("guava-14.0-rc3.jar", "931ae21fa8014c3ce686aaa621eae565fefb1a6a", False), + FMLLib("asm-all-4.1.jar", "054986e962b88d8660ae4566475658469595ef58", False), + FMLLib("bcprov-jdk15on-148.jar", "960dea7c9181ba0b17e8bab0c06a43f0a5f04e65", True), + FMLLib("deobfuscation_data_1.5.2.zip", "446e55cd986582c70fcf12cb27bc00114c5adfd9", False), + FMLLib("scala-library.jar", "458d046151ad179c85429ed7420ffb1eaf6ddf85", True) +] + +''' +"install": { + "profileName": "Forge", + "target":"Forge8.9.0.753", + "path":"net.minecraftforge:minecraftforge:8.9.0.753", + "version":"Forge 8.9.0.753", + "filePath":"minecraftforge-universal-1.6.1-8.9.0.753.jar", + "welcome":"Welcome to the simple Forge installer.", + "minecraft":"1.6.1", + "logo":"/big_logo.png", + "mirrorList": "http://files.minecraftforge.net/mirror-brand.list" +}, +"install": { + "profileName": "forge", + "target":"1.11-forge1.11-13.19.0.2141", + "path":"net.minecraftforge:forge:1.11-13.19.0.2141", + "version":"forge 1.11-13.19.0.2141", + "filePath":"forge-1.11-13.19.0.2141-universal.jar", + "welcome":"Welcome to the simple forge installer.", + "minecraft":"1.11", + "mirrorList" : "http://files.minecraftforge.net/mirror-brand.list", + "logo":"/big_logo.png", + "modList":"none" +}, +''' + + +class ForgeInstallerProfileInstallSection(JsonObject): + profileName = StringProperty(required=True) + target = StringProperty(required=True) + path = GradleSpecifierProperty(required=True) + version = StringProperty(required=True) + filePath = StringProperty(required=True) + welcome = StringProperty(required=True) + minecraft = StringProperty(required=True) + logo = StringProperty(required=True) + mirrorList = StringProperty(required=True) + modList = StringProperty(exclude_if_none=True, default=None) + + +class ForgeLibrary(MojangLibrary): + url = StringProperty(exclude_if_none=True) + serverreq = BooleanProperty(exclude_if_none=True, default=None) + clientreq = BooleanProperty(exclude_if_none=True, default=None) + checksums = ListProperty(StringProperty) + comment = StringProperty() + + +class ForgeVersionFile(MojangVersionFile): + libraries = ListProperty(ForgeLibrary, exclude_if_none=True, default=None) # overrides Mojang libraries + inheritsFrom = StringProperty() + jar = StringProperty() + + +''' +"optionals": [ + { + "name": "Mercurius", + "client": true, + "server": true, + "default": true, + "inject": true, + "desc": "A mod that collects statistics about Minecraft and your system.<br>Useful for Forge to understand how Minecraft/Forge are used.", + "url": "http://www.minecraftforge.net/forum/index.php?topic=43278.0", + "artifact": "net.minecraftforge:MercuriusUpdater:1.11.2", + "maven": "http://files.minecraftforge.net/maven/" + } +] +''' + + +class ForgeOptional(JsonObject): + name = StringProperty() + client = BooleanProperty() + server = BooleanProperty() + default = BooleanProperty() + inject = BooleanProperty() + desc = StringProperty() + url = StringProperty() + artifact = GradleSpecifierProperty() + maven = StringProperty() + + +class ForgeInstallerProfile(JsonObject): + install = ObjectProperty(ForgeInstallerProfileInstallSection, required=True) + versionInfo = ObjectProperty(ForgeVersionFile, required=True) + optionals = ListProperty(ForgeOptional) + + +class ForgeLegacyInfo(JsonObject): + releaseTime = ISOTimestampProperty() + size = IntegerProperty() + sha256 = StringProperty() + sha1 = StringProperty() + + +class ForgeLegacyInfoList(JsonObject): + number = DictProperty(ForgeLegacyInfo) + + +class DataSpec(JsonObject): + client = StringProperty() + server = StringProperty() + + +class ProcessorSpec(JsonObject): + jar = StringProperty() + classpath = ListProperty(StringProperty) + args = ListProperty(StringProperty) + outputs = DictProperty(StringProperty) + sides = ListProperty(StringProperty, exclude_if_none=True, default=None) + + +# Note: This is only used in one version (1.12.2-14.23.5.2851) and we don't even use the installer profile in it. +# It's here just so it parses and we can continue... +class ForgeInstallerProfileV1_5(JsonObject): + _comment = ListProperty(StringProperty) + spec = IntegerProperty() + profile = StringProperty() + version = StringProperty() + icon = StringProperty() + json = StringProperty() + path = GradleSpecifierProperty() + logo = StringProperty() + minecraft = StringProperty() + welcome = StringProperty() + # We don't know what 'data' actually is in this one. It's an empty array + data = ListProperty(StringProperty) + processors = ListProperty(ProcessorSpec) + libraries = ListProperty(MojangLibrary) + mirrorList = StringProperty(exclude_if_none=True, default=None) + + +class ForgeInstallerProfileV2(JsonObject): + _comment = ListProperty(StringProperty) + spec = IntegerProperty() + profile = StringProperty() + version = StringProperty() + icon = StringProperty() + json = StringProperty() + path = GradleSpecifierProperty() + logo = StringProperty() + minecraft = StringProperty() + welcome = StringProperty() + data = DictProperty(DataSpec) + processors = ListProperty(ProcessorSpec) + libraries = ListProperty(MojangLibrary) + mirrorList = StringProperty(exclude_if_none=True, default=None) + serverJarPath = StringProperty(exclude_if_none=True, default=None) + + +class InstallerInfo(JsonObject): + sha1hash = StringProperty() + sha256hash = StringProperty() + size = IntegerProperty() diff --git a/meta/jsonobject/__init__.py b/meta/jsonobject/__init__.py new file mode 100644 index 0000000000..83f41913c4 --- /dev/null +++ b/meta/jsonobject/__init__.py @@ -0,0 +1,17 @@ +# TODO: maybe move to pydantic in the future? + +from __future__ import absolute_import +from .base import JsonObjectMeta +from .containers import JsonArray +from .properties import * +from .base_properties import * +from .api import JsonObject + +__all__ = [ + 'IntegerProperty', 'FloatProperty', 'DecimalProperty', + 'StringProperty', 'BooleanProperty', + 'DateProperty', 'DateTimeProperty', 'TimeProperty', + 'ObjectProperty', 'ListProperty', 'DictProperty', 'SetProperty', + 'JsonObject', 'JsonArray', 'AbstractDateProperty', 'JsonProperty', + 'DefaultProperty' +] diff --git a/meta/jsonobject/api.py b/meta/jsonobject/api.py new file mode 100644 index 0000000000..8b9c4767c1 --- /dev/null +++ b/meta/jsonobject/api.py @@ -0,0 +1,53 @@ +from __future__ import absolute_import +from .base import JsonObjectBase, _LimitedDictInterfaceMixin + +import six +import decimal +import datetime + +from . import properties +import re + +re_date = re.compile(r'^(\d{4})\D?(0[1-9]|1[0-2])\D?([12]\d|0[1-9]|3[01])$') +re_time = re.compile( + r'^([01]\d|2[0-3])\D?([0-5]\d)\D?([0-5]\d)?\D?(\d{3,6})?$') +re_datetime = re.compile( + r'^(\d{4})\D?(0[1-9]|1[0-2])\D?([12]\d|0[1-9]|3[01])' + r'(\D?([01]\d|2[0-3])\D?([0-5]\d)\D?([0-5]\d)?\D?(\d{3,6})?' + r'([zZ]|([\+-])([01]\d|2[0-3])\D?([0-5]\d)?)?)?$' +) +re_decimal = re.compile('^(\d+)\.(\d+)$') +if six.PY3: + unicode = str + long = int + + +class JsonObject(JsonObjectBase, _LimitedDictInterfaceMixin): + def __getstate__(self): + return self.to_json() + + def __setstate__(self, dct): + self.__init__(dct) + + class Meta(object): + properties = { + decimal.Decimal: properties.DecimalProperty, + datetime.datetime: properties.DateTimeProperty, + datetime.date: properties.DateProperty, + datetime.time: properties.TimeProperty, + str: properties.StringProperty, + unicode: properties.StringProperty, + bool: properties.BooleanProperty, + int: properties.IntegerProperty, + long: properties.IntegerProperty, + float: properties.FloatProperty, + list: properties.ListProperty, + dict: properties.DictProperty, + set: properties.SetProperty, + } + string_conversions = ( + (re_date, datetime.date), + (re_time, datetime.time), + (re_datetime, datetime.datetime), + (re_decimal, decimal.Decimal), + ) diff --git a/meta/jsonobject/base.py b/meta/jsonobject/base.py new file mode 100644 index 0000000000..90a5f44c6a --- /dev/null +++ b/meta/jsonobject/base.py @@ -0,0 +1,394 @@ +from __future__ import absolute_import +from collections import namedtuple, OrderedDict +import copy +import six +import inspect +from .exceptions import ( + DeleteNotAllowed, + WrappingAttributeError, +) +from .base_properties import JsonProperty, DefaultProperty +from .utils import check_type + +JsonObjectClassSettings = namedtuple('JsonObjectClassSettings', ['type_config']) + +CLASS_SETTINGS_ATTR = '_$_class_settings' + + +def get_settings(cls): + return getattr(cls, CLASS_SETTINGS_ATTR, + JsonObjectClassSettings(type_config=TypeConfig())) + + +def set_settings(cls, settings): + setattr(cls, CLASS_SETTINGS_ATTR, settings) + + +class TypeConfig(object): + """ + This class allows the user to configure dynamic + type handlers and string conversions for their JsonObject. + + properties is a map from python types to JsonProperty subclasses + string_conversions is a list or tuple of (regex, python type)-tuples + + This class is used to store the configuration but is not part of the API. + To configure: + + class Foo(JsonObject): + # property definitions go here + # ... + + class Meta(object): + update_properties = { + datetime.datetime: MySpecialDateTimeProperty + } + # this is already set by default + # but you can override with your own modifications + string_conversions = ((date_re, datetime.date), + (datetime_re, datetime.datetime), + (time_re, datetime.time), + (decimal_re, decimal.Decimal)) + + If you now do + + foo = Foo() + foo.timestamp = datetime.datetime(1988, 7, 7, 11, 8, 0) + + timestamp will be governed by a MySpecialDateTimeProperty + instead of the default. + + """ + + def __init__(self, properties=None, string_conversions=None): + self._properties = properties if properties is not None else {} + + self._string_conversions = ( + OrderedDict(string_conversions) if string_conversions is not None + else OrderedDict() + ) + # cache this + self.string_conversions = self._get_string_conversions() + self.properties = self._properties + + def replace(self, properties=None, string_conversions=None): + return TypeConfig( + properties=(properties if properties is not None + else self._properties), + string_conversions=(string_conversions if string_conversions is not None + else self._string_conversions) + ) + + def updated(self, properties=None, string_conversions=None): + """ + update properties and string_conversions with the paramenters + keeping all non-mentioned items the same as before + returns a new TypeConfig with these changes + (does not modify original) + + """ + _properties = self._properties.copy() + _string_conversions = self.string_conversions[:] + if properties: + _properties.update(properties) + if string_conversions: + _string_conversions.extend(string_conversions) + return TypeConfig( + properties=_properties, + string_conversions=_string_conversions, + ) + + def _get_string_conversions(self): + result = [] + for pattern, conversion in self._string_conversions.items(): + conversion = ( + conversion if conversion not in self._properties + else self._properties[conversion](type_config=self).to_python + ) + result.append((pattern, conversion)) + return result + + +META_ATTRS = ('properties', 'string_conversions', 'update_properties') + + +class JsonObjectMeta(type): + class Meta(object): + pass + + def __new__(mcs, name, bases, dct): + cls = super(JsonObjectMeta, mcs).__new__(mcs, name, bases, dct) + + cls.__configure(**{key: value + for key, value in cls.Meta.__dict__.items() + if key in META_ATTRS}) + cls_settings = get_settings(cls) + + properties = {} + properties_by_name = {} + for key, value in dct.items(): + if isinstance(value, JsonProperty): + properties[key] = value + elif key.startswith('_'): + continue + elif type(value) in cls_settings.type_config.properties: + property_ = cls_settings.type_config.properties[type(value)](default=value) + properties[key] = dct[key] = property_ + setattr(cls, key, property_) + + for key, property_ in properties.items(): + property_.init_property(default_name=key, + type_config=cls_settings.type_config) + assert property_.name is not None, property_ + assert property_.name not in properties_by_name, \ + 'You can only have one property named {0}'.format( + property_.name) + properties_by_name[property_.name] = property_ + + for base in bases: + if getattr(base, '_properties_by_attr', None): + for key, value in base._properties_by_attr.items(): + if key not in properties: + properties[key] = value + properties_by_name[value.name] = value + + cls._properties_by_attr = properties + cls._properties_by_key = properties_by_name + return cls + + def __configure(cls, properties=None, string_conversions=None, + update_properties=None): + super_settings = get_settings(super(cls, cls)) + assert not properties or not update_properties, \ + "{} {}".format(properties, update_properties) + type_config = super_settings.type_config + if update_properties is not None: + type_config = type_config.updated(properties=update_properties) + elif properties is not None: + type_config = type_config.replace(properties=properties) + if string_conversions is not None: + type_config = type_config.replace( + string_conversions=string_conversions) + set_settings(cls, super_settings._replace(type_config=type_config)) + return cls + + +class _JsonObjectPrivateInstanceVariables(object): + + def __init__(self, dynamic_properties=None): + self.dynamic_properties = dynamic_properties or {} + + +@six.add_metaclass(JsonObjectMeta) +class JsonObjectBase(object): + _allow_dynamic_properties = False + _validate_required_lazily = False + + _properties_by_attr = None + _properties_by_key = None + + _string_conversions = () + + def __init__(self, _obj=None, **kwargs): + setattr(self, '_$', _JsonObjectPrivateInstanceVariables()) + + self._obj = check_type(_obj, dict, + 'JsonObject must wrap a dict or None') + self._wrapped = {} + + for key, value in self._obj.items(): + try: + self.set_raw_value(key, value) + except AttributeError: + raise WrappingAttributeError( + "can't set attribute corresponding to {key!r} " + "on a {cls} while wrapping {data!r}".format( + cls=self.__class__, + key=key, + data=_obj, + ) + ) + + for attr, value in kwargs.items(): + try: + setattr(self, attr, value) + except AttributeError: + raise WrappingAttributeError( + "can't set attribute {key!r} " + "on a {cls} while wrapping {data!r}".format( + cls=self.__class__, + key=attr, + data=_obj, + ) + ) + + for key, value in self._properties_by_key.items(): + if key not in self._obj: + try: + d = value.default() + except TypeError: + d = value.default(self) + self[key] = d + + def set_raw_value(self, key, value): + wrapped = self.__wrap(key, value) + if key in self._properties_by_key: + self[key] = wrapped + else: + setattr(self, key, wrapped) + + @classmethod + def properties(cls): + return cls._properties_by_attr.copy() + + @property + def __dynamic_properties(self): + return getattr(self, '_$').dynamic_properties + + @classmethod + def wrap(cls, obj): + self = cls(obj) + return self + + def validate(self, required=True): + for key, value in self._wrapped.items(): + self.__get_property(key).validate(value, required=required) + + def to_json(self): + self.validate() + return copy.deepcopy(self._obj) + + def __get_property(self, key): + try: + return self._properties_by_key[key] + except KeyError: + return DefaultProperty(type_config=get_settings(self).type_config) + + def __wrap(self, key, value): + property_ = self.__get_property(key) + + if value is None: + return None + + return property_.wrap(value) + + def __unwrap(self, key, value): + property_ = self.__get_property(key) + try: + property_.validate( + value, + required=not self._validate_required_lazily, + recursive=False, + ) + except TypeError: + property_.validate( + value, + required=not self._validate_required_lazily, + ) + if value is None: + return None, None + + return property_.unwrap(value) + + def __setitem__(self, key, value): + wrapped, unwrapped = self.__unwrap(key, value) + self._wrapped[key] = wrapped + if self.__get_property(key).exclude(unwrapped): + self._obj.pop(key, None) + else: + self._obj[key] = unwrapped + if key not in self._properties_by_key: + assert key not in self._properties_by_attr + self.__dynamic_properties[key] = wrapped + super(JsonObjectBase, self).__setattr__(key, wrapped) + + def __is_dynamic_property(self, name): + return ( + name not in self._properties_by_attr and + not name.startswith('_') and + not inspect.isdatadescriptor(getattr(self.__class__, name, None)) + ) + + def __setattr__(self, name, value): + if self.__is_dynamic_property(name): + if self._allow_dynamic_properties: + self[name] = value + else: + raise AttributeError( + "{0!r} is not defined in schema " + "(not a valid property)".format(name) + ) + else: + super(JsonObjectBase, self).__setattr__(name, value) + + def __delitem__(self, key): + if key in self._properties_by_key: + raise DeleteNotAllowed(key) + else: + if not self.__is_dynamic_property(key): + raise KeyError(key) + del self._obj[key] + del self._wrapped[key] + del self.__dynamic_properties[key] + super(JsonObjectBase, self).__delattr__(key) + + def __delattr__(self, name): + if name in self._properties_by_attr: + raise DeleteNotAllowed(name) + elif self.__is_dynamic_property(name): + del self[name] + else: + super(JsonObjectBase, self).__delattr__(name) + + def __repr__(self): + name = self.__class__.__name__ + predefined_properties = self._properties_by_attr.keys() + predefined_property_keys = set(self._properties_by_attr[p].name + for p in predefined_properties) + dynamic_properties = (set(self._wrapped.keys()) + - predefined_property_keys) + properties = sorted(predefined_properties) + sorted(dynamic_properties) + return u'{name}({keyword_args})'.format( + name=name, + keyword_args=', '.join('{key}={value!r}'.format( + key=key, + value=getattr(self, key) + ) for key in properties), + ) + + +class _LimitedDictInterfaceMixin(object): + """ + mindlessly farms selected dict methods out to an internal dict + + really only a separate class from JsonObject + to keep this mindlessness separate from the methods + that need to be more carefully understood + + """ + _wrapped = None + + def keys(self): + return self._wrapped.keys() + + def items(self): + return self._wrapped.items() + + def iteritems(self): + return self._wrapped.iteritems() + + def __contains__(self, item): + return item in self._wrapped + + def __getitem__(self, item): + return self._wrapped[item] + + def __iter__(self): + return iter(self._wrapped) + + def __len__(self): + return len(self._wrapped) + + +def get_dynamic_properties(obj): + return getattr(obj, '_$').dynamic_properties.copy() diff --git a/meta/jsonobject/base_properties.py b/meta/jsonobject/base_properties.py new file mode 100644 index 0000000000..b43b1d44e2 --- /dev/null +++ b/meta/jsonobject/base_properties.py @@ -0,0 +1,320 @@ +from __future__ import absolute_import +import six +import inspect +from .exceptions import BadValueError + +function_name = None +if six.PY3: + def function_name(f): + return f.__name__ +else: + def function_name(f): + return f.func_name + + +class JsonProperty(object): + default = None + type_config = None + + def __init__(self, default=Ellipsis, name=None, choices=None, + required=False, exclude_if_none=False, validators=None, + verbose_name=None, type_config=None): + validators = validators or () + self.name = name + if default is Ellipsis: + default = self.default + if callable(default): + self.default = default + else: + self.default = lambda: default + self.choices = choices + self.choice_keys = [] + if choices: + for choice in choices: + if isinstance(choice, tuple): + choice, _ = choice + self.choice_keys.append(choice) + self.required = required + self.exclude_if_none = exclude_if_none + self._validators = validators + self.verbose_name = verbose_name + if type_config: + self.type_config = type_config + + def init_property(self, default_name, type_config): + self.name = self.name or default_name + self.type_config = self.type_config or type_config + + def wrap(self, obj): + raise NotImplementedError() + + def unwrap(self, obj): + """ + must return tuple of (wrapped, unwrapped) + + If obj is already a fully wrapped object, + it must be returned as the first element. + + For an example where the first element is relevant see ListProperty + + """ + raise NotImplementedError() + + def to_json(self, value): + _, unwrapped = self.unwrap(value) + return unwrapped + + def to_python(self, value): + return self.wrap(value) + + def __get__(self, instance, owner): + if instance: + assert self.name in instance + return instance[self.name] + else: + return self + + def __set__(self, instance, value): + instance[self.name] = value + + def __call__(self, method): + """ + use a property as a decorator to set its default value + + class Document(JsonObject): + @StringProperty() + def doc_type(self): + return self.__class__.__name__ + """ + assert self.default() is None + self.default = method + self.name = self.name or function_name(method) + return self + + def exclude(self, value): + return self.exclude_if_none and value == None + + def empty(self, value): + return value is None + + def validate(self, value, required=True, recursive=True): + if (self.choice_keys and value not in self.choice_keys + and value is not None): + raise BadValueError( + '{0!r} not in choices: {1!r}'.format(value, self.choice_keys) + ) + + if not self.empty(value): + self._custom_validate(value) + elif required and self.required: + raise BadValueError( + 'Property {0} is required.'.format(self.name) + ) + if recursive and hasattr(value, 'validate'): + value.validate(required=required) + + def _custom_validate(self, value): + if self._validators: + if hasattr(self._validators, '__iter__'): + for validator in self._validators: + validator(value) + else: + self._validators(value) + + +class JsonContainerProperty(JsonProperty): + _type = default = None + container_class = None + + def __init__(self, item_type=None, **kwargs): + self._item_type_deferred = item_type + super(JsonContainerProperty, self).__init__(**kwargs) + + def init_property(self, **kwargs): + super(JsonContainerProperty, self).init_property(**kwargs) + if not inspect.isfunction(self._item_type_deferred): + # trigger validation + self.item_type + + def set_item_type(self, item_type): + from meta.jsonobject.base import JsonObjectMeta + if hasattr(item_type, '_type'): + item_type = item_type._type + if isinstance(item_type, tuple): + # this is for the case where item_type = (int, long) + item_type = item_type[0] + allowed_types = set(self.type_config.properties.keys()) + if isinstance(item_type, JsonObjectMeta) \ + or not item_type or item_type in allowed_types: + self._item_type = item_type + else: + raise ValueError("item_type {0!r} not in {1!r}".format( + item_type, + allowed_types, + )) + + @property + def item_type(self): + if hasattr(self, '_item_type_deferred'): + if inspect.isfunction(self._item_type_deferred): + self.set_item_type(self._item_type_deferred()) + else: + self.set_item_type(self._item_type_deferred) + del self._item_type_deferred + return self._item_type + + def empty(self, value): + return not value + + def wrap(self, obj): + wrapper = self.type_to_property(self.item_type) if self.item_type else None + return self.container_class(obj, wrapper=wrapper, + type_config=self.type_config) + + def type_to_property(self, item_type): + map_types_properties = self.type_config.properties + from .properties import ObjectProperty + from .base import JsonObjectBase + if issubclass(item_type, JsonObjectBase): + return ObjectProperty(item_type, type_config=self.type_config) + elif item_type in map_types_properties: + return map_types_properties[item_type](type_config=self.type_config) + else: + for key, value in map_types_properties.items(): + if issubclass(item_type, key): + return value(type_config=self.type_config) + raise TypeError('Type {0} not recognized'.format(item_type)) + + def unwrap(self, obj): + if not isinstance(obj, self._type): + raise BadValueError( + '{0!r} is not an instance of {1!r}'.format( + obj, self._type.__name__) + ) + if isinstance(obj, self.container_class): + return obj, obj._obj + else: + wrapped = self.wrap(self._type()) + self._update(wrapped, obj) + return self.unwrap(wrapped) + + def _update(self, container, extension): + raise NotImplementedError() + + +class DefaultProperty(JsonProperty): + + def wrap(self, obj): + assert self.type_config.string_conversions is not None + value = self.value_to_python(obj) + property_ = self.value_to_property(value) + + if property_: + return property_.wrap(obj) + + def unwrap(self, obj): + property_ = self.value_to_property(obj) + if property_: + return property_.unwrap(obj) + else: + return obj, None + + def value_to_property(self, value): + map_types_properties = self.type_config.properties + if value is None: + return None + elif type(value) in map_types_properties: + return map_types_properties[type(value)]( + type_config=self.type_config) + else: + for value_type, prop_class in map_types_properties.items(): + if isinstance(value, value_type): + return prop_class(type_config=self.type_config) + else: + raise BadValueError( + 'value {0!r} not in allowed types: {1!r}'.format( + value, map_types_properties.keys()) + ) + + def value_to_python(self, value): + """ + convert encoded string values to the proper python type + + ex: + >>> DefaultProperty().value_to_python('2013-10-09T10:05:51Z') + datetime.datetime(2013, 10, 9, 10, 5, 51) + + other values will be passed through unmodified + Note: containers' items are NOT recursively converted + + """ + if isinstance(value, six.string_types): + convert = None + for pattern, _convert in self.type_config.string_conversions: + if pattern.match(value): + convert = _convert + break + + if convert is not None: + try: + # sometimes regex fail so return value + value = convert(value) + except Exception: + pass + return value + + +class AssertTypeProperty(JsonProperty): + _type = None + + def assert_type(self, obj): + if not isinstance(obj, self._type): + raise BadValueError( + '{0!r} not of type {1!r}'.format(obj, self._type) + ) + + def selective_coerce(self, obj): + return obj + + def wrap(self, obj): + obj = self.selective_coerce(obj) + self.assert_type(obj) + return obj + + def unwrap(self, obj): + obj = self.selective_coerce(obj) + self.assert_type(obj) + return obj, obj + + +class AbstractDateProperty(JsonProperty): + _type = None + + def __init__(self, exact=False, *args, **kwargs): + super(AbstractDateProperty, self).__init__(*args, **kwargs) + self.exact = exact + + def wrap(self, obj): + try: + if not isinstance(obj, six.string_types): + raise ValueError() + return self._wrap(obj) + except ValueError: + raise BadValueError('{0!r} is not a {1}-formatted string'.format( + obj, + self._type.__name__, + )) + + def unwrap(self, obj): + if not isinstance(obj, self._type): + raise BadValueError('{0!r} is not a {1} object'.format( + obj, + self._type.__name__, + )) + return self._unwrap(obj) + + def _wrap(self, obj): + raise NotImplementedError() + + def _unwrap(self, obj): + raise NotImplementedError() diff --git a/meta/jsonobject/containers.py b/meta/jsonobject/containers.py new file mode 100644 index 0000000000..1150ab9d23 --- /dev/null +++ b/meta/jsonobject/containers.py @@ -0,0 +1,252 @@ +from __future__ import absolute_import +from .base_properties import DefaultProperty +from .utils import check_type, SimpleDict +import copy + + +class JsonArray(list): + def __init__(self, _obj=None, wrapper=None, type_config=None): + super(JsonArray, self).__init__() + self._obj = check_type(_obj, list, + 'JsonArray must wrap a list or None') + + assert type_config is not None + self._type_config = type_config + self._wrapper = ( + wrapper or + DefaultProperty(type_config=self._type_config) + ) + for item in self._obj: + super(JsonArray, self).append(self._wrapper.wrap(item)) + + def validate(self, required=True): + for obj in self: + self._wrapper.validate(obj, required=required) + + def to_json(self): + self.validate() + return copy.deepcopy(self._obj) + + def append(self, wrapped): + wrapped, unwrapped = self._wrapper.unwrap(wrapped) + self._obj.append(unwrapped) + super(JsonArray, self).append(wrapped) + + def __delitem__(self, i): + super(JsonArray, self).__delitem__(i) + del self._obj[i] + + def __setitem__(self, i, wrapped): + wrapped, unwrapped = self._wrapper.unwrap(wrapped) + self._obj[i] = unwrapped + super(JsonArray, self).__setitem__(i, wrapped) + + def extend(self, wrapped_list): + if wrapped_list: + wrapped_list, unwrapped_list = zip( + *map(self._wrapper.unwrap, wrapped_list) + ) + else: + unwrapped_list = [] + self._obj.extend(unwrapped_list) + super(JsonArray, self).extend(wrapped_list) + + def insert(self, index, wrapped): + wrapped, unwrapped = self._wrapper.unwrap(wrapped) + self._obj.insert(index, unwrapped) + super(JsonArray, self).insert(index, wrapped) + + def remove(self, value): + i = self.index(value) + super(JsonArray, self).remove(value) + self._obj.pop(i) + + def pop(self, index=-1): + self._obj.pop(index) + return super(JsonArray, self).pop(index) + + def sort(self, cmp=None, key=None, reverse=False): + zipped = zip(self, self._obj) + if key: + new_key = lambda pair: key(pair[0]) + zipped.sort(key=new_key, reverse=reverse) + elif cmp: + new_cmp = lambda pair1, pair2: cmp(pair1[0], pair2[0]) + zipped.sort(cmp=new_cmp, reverse=reverse) + else: + zipped.sort(reverse=reverse) + + wrapped_list, unwrapped_list = zip(*zipped) + while self: + self.pop() + super(JsonArray, self).extend(wrapped_list) + self._obj.extend(unwrapped_list) + + def reverse(self): + self._obj.reverse() + super(JsonArray, self).reverse() + + def __fix_slice(self, i, j): + length = len(self) + if j < 0: + j += length + if i < 0: + i += length + if i > length: + i = length + if j > length: + j = length + return i, j + + def __setslice__(self, i, j, sequence): + i, j = self.__fix_slice(i, j) + for _ in range(j - i): + self.pop(i) + for k, wrapped in enumerate(sequence): + self.insert(i + k, wrapped) + + def __delslice__(self, i, j): + i, j = self.__fix_slice(i, j) + for _ in range(j - i): + self.pop(i) + + +class JsonDict(SimpleDict): + + def __init__(self, _obj=None, wrapper=None, type_config=None): + super(JsonDict, self).__init__() + self._obj = check_type(_obj, dict, 'JsonDict must wrap a dict or None') + assert type_config is not None + self._type_config = type_config + self._wrapper = ( + wrapper or + DefaultProperty(type_config=self._type_config) + ) + for key, value in self._obj.items(): + self[key] = self.__wrap(key, value) + + def validate(self, required=True): + for obj in self.values(): + self._wrapper.validate(obj, required=required) + + def __wrap(self, key, unwrapped): + return self._wrapper.wrap(unwrapped) + + def __unwrap(self, key, wrapped): + return self._wrapper.unwrap(wrapped) + + def __setitem__(self, key, value): + if isinstance(key, int): + key = str(key) + + wrapped, unwrapped = self.__unwrap(key, value) + self._obj[key] = unwrapped + super(JsonDict, self).__setitem__(key, wrapped) + + def __delitem__(self, key): + del self._obj[key] + super(JsonDict, self).__delitem__(key) + + def __getitem__(self, key): + if isinstance(key, int): + key = str(key) + return super(JsonDict, self).__getitem__(key) + + +class JsonSet(set): + def __init__(self, _obj=None, wrapper=None, type_config=None): + super(JsonSet, self).__init__() + if isinstance(_obj, set): + _obj = list(_obj) + self._obj = check_type(_obj, list, 'JsonSet must wrap a list or None') + assert type_config is not None + self._type_config = type_config + self._wrapper = ( + wrapper or + DefaultProperty(type_config=self._type_config) + ) + for item in self._obj: + super(JsonSet, self).add(self._wrapper.wrap(item)) + + def validate(self, required=True): + for obj in self: + self._wrapper.validate(obj, required=required) + + def add(self, wrapped): + wrapped, unwrapped = self._wrapper.unwrap(wrapped) + if wrapped not in self: + self._obj.append(unwrapped) + super(JsonSet, self).add(wrapped) + + def remove(self, wrapped): + wrapped, unwrapped = self._wrapper.unwrap(wrapped) + if wrapped in self: + self._obj.remove(unwrapped) + super(JsonSet, self).remove(wrapped) + else: + raise KeyError(wrapped) + + def discard(self, wrapped): + try: + self.remove(wrapped) + except KeyError: + pass + + def pop(self): + # get first item + for wrapped in self: + break + else: + raise KeyError() + wrapped_, unwrapped = self._wrapper.unwrap(wrapped) + assert wrapped is wrapped_ + self.remove(unwrapped) + return wrapped + + def clear(self): + while self: + self.pop() + + def __ior__(self, other): + for wrapped in other: + self.add(wrapped) + return self + + def update(self, *args): + for wrapped_list in args: + self |= set(wrapped_list) + + union_update = update + + def __iand__(self, other): + for wrapped in list(self): + if wrapped not in other: + self.remove(wrapped) + return self + + def intersection_update(self, *args): + for wrapped_list in args: + self &= set(wrapped_list) + + def __isub__(self, other): + for wrapped in list(self): + if wrapped in other: + self.remove(wrapped) + return self + + def difference_update(self, *args): + for wrapped_list in args: + self -= set(wrapped_list) + + def __ixor__(self, other): + removed = set() + for wrapped in list(self): + if wrapped in other: + self.remove(wrapped) + removed.add(wrapped) + self.update(other - removed) + return self + + def symmetric_difference_update(self, *args): + for wrapped_list in args: + self ^= set(wrapped_list) diff --git a/meta/jsonobject/exceptions.py b/meta/jsonobject/exceptions.py new file mode 100644 index 0000000000..a42022e120 --- /dev/null +++ b/meta/jsonobject/exceptions.py @@ -0,0 +1,10 @@ +class DeleteNotAllowed(Exception): + pass + + +class BadValueError(Exception): + """raised when a value can't be validated or is required""" + + +class WrappingAttributeError(AttributeError): + pass diff --git a/meta/jsonobject/properties.py b/meta/jsonobject/properties.py new file mode 100644 index 0000000000..05bba86da9 --- /dev/null +++ b/meta/jsonobject/properties.py @@ -0,0 +1,155 @@ +# DateTimeProperty, DateProperty, and TimeProperty +# include code copied from couchdbkit +from __future__ import absolute_import +import sys +import datetime +import time +import decimal +from .base_properties import ( + AbstractDateProperty, + AssertTypeProperty, + JsonContainerProperty, + JsonProperty, + DefaultProperty, +) +from .containers import JsonArray, JsonDict, JsonSet + +if sys.version > '3': + unicode = str + long = int + + +class StringProperty(AssertTypeProperty): + _type = (unicode, str) + + def selective_coerce(self, obj): + if isinstance(obj, str): + obj = unicode(obj) + return obj + + +class BooleanProperty(AssertTypeProperty): + _type = bool + + +class IntegerProperty(AssertTypeProperty): + _type = (int, long) + + +class FloatProperty(AssertTypeProperty): + _type = float + + def selective_coerce(self, obj): + if isinstance(obj, (int, long)): + obj = float(obj) + return obj + + +class DecimalProperty(JsonProperty): + + def wrap(self, obj): + return decimal.Decimal(obj) + + def unwrap(self, obj): + if isinstance(obj, (int, long)): + obj = decimal.Decimal(obj) + elif isinstance(obj, float): + # python 2.6 doesn't allow a float to Decimal + obj = decimal.Decimal(unicode(obj)) + assert isinstance(obj, decimal.Decimal) + return obj, unicode(obj) + + +class DateProperty(AbstractDateProperty): + _type = datetime.date + + def _wrap(self, value): + fmt = '%Y-%m-%d' + try: + return datetime.date(*time.strptime(value, fmt)[:3]) + except ValueError as e: + raise ValueError('Invalid ISO date {0!r} [{1}]'.format(value, e)) + + def _unwrap(self, value): + return value, value.isoformat() + + +class DateTimeProperty(AbstractDateProperty): + _type = datetime.datetime + + def _wrap(self, value): + if not self.exact: + value = value.split('.', 1)[0] # strip out microseconds + value = value[0:19] # remove timezone + fmt = '%Y-%m-%dT%H:%M:%S' + else: + fmt = '%Y-%m-%dT%H:%M:%S.%fZ' + try: + return datetime.datetime.strptime(value, fmt) + except ValueError as e: + raise ValueError( + 'Invalid ISO date/time {0!r} [{1}]'.format(value, e)) + + def _unwrap(self, value): + if not self.exact: + value = value.replace(microsecond=0) + padding = '' + else: + padding = '' if value.microsecond else '.000000' + return value, value.isoformat() + padding + 'Z' + + +class TimeProperty(AbstractDateProperty): + _type = datetime.time + + def _wrap(self, value): + if not self.exact: + value = value.split('.', 1)[0] # strip out microseconds + fmt = '%H:%M:%S' + else: + fmt = '%H:%M:%S.%f' + try: + return datetime.time(*time.strptime(value, fmt)[3:6]) + except ValueError as e: + raise ValueError('Invalid ISO time {0!r} [{1}]'.format(value, e)) + + def _unwrap(self, value): + if not self.exact: + value = value.replace(microsecond=0) + return value, value.isoformat() + + +class ObjectProperty(JsonContainerProperty): + default = lambda self: self.item_type() + + def wrap(self, obj, string_conversions=None): + return self.item_type.wrap(obj) + + def unwrap(self, obj): + assert isinstance(obj, self.item_type), \ + '{0} is not an instance of {1}'.format(obj, self.item_type) + return obj, obj._obj + + +class ListProperty(JsonContainerProperty): + _type = default = list + container_class = JsonArray + + def _update(self, container, extension): + container.extend(extension) + + +class DictProperty(JsonContainerProperty): + _type = default = dict + container_class = JsonDict + + def _update(self, container, extension): + container.update(extension) + + +class SetProperty(JsonContainerProperty): + _type = default = set + container_class = JsonSet + + def _update(self, container, extension): + container.update(extension) diff --git a/meta/jsonobject/utils.py b/meta/jsonobject/utils.py new file mode 100644 index 0000000000..9ee8569801 --- /dev/null +++ b/meta/jsonobject/utils.py @@ -0,0 +1,57 @@ +from __future__ import absolute_import +from .exceptions import BadValueError + + +def check_type(obj, item_type, message): + if obj is None: + return item_type() + elif not isinstance(obj, item_type): + raise BadValueError('{}. Found object of type: {}'.format(message, type(obj))) + else: + return obj + + +class SimpleDict(dict): + """ + Re-implements destructive methods of dict + to use only setitem and getitem and delitem + """ + + def update(self, E=None, **F): + for dct in (E, F): + if dct: + for key, value in dct.items(): + self[key] = value + + def clear(self): + for key in list(self.keys()): + del self[key] + + def pop(self, key, *args): + if len(args) > 1: + raise TypeError('pop expected at most 2 arguments, got 3') + try: + val = self[key] + del self[key] + return val + except KeyError: + try: + return args[0] + except IndexError: + raise KeyError(key) + + def popitem(self): + try: + arbitrary_key = list(self.keys())[0] + except IndexError: + raise KeyError('popitem(): dictionary is empty') + val = self[arbitrary_key] + del self[arbitrary_key] + return (arbitrary_key, val) + + def setdefault(self, key, default=None): + try: + return self[key] + except KeyError: + self[key] = default + return default diff --git a/meta/liteloaderutil.py b/meta/liteloaderutil.py new file mode 100644 index 0000000000..dce6cb19d4 --- /dev/null +++ b/meta/liteloaderutil.py @@ -0,0 +1,118 @@ +from .metautil import * + +''' + "repo":{ + "stream":"RELEASE", + "type":"m2", + "url":"http:\/\/dl.liteloader.com\/repo\/", + "classifier":"" + }, +''' + + +class LiteloaderRepo(JsonObject): + stream = StringProperty(required=True) + type = StringProperty(required=True) + url = StringProperty(required=True) + classifier = StringProperty(required=True) + + +''' + "53639d52340479ccf206a04f5e16606f":{ + "tweakClass":"com.mumfrey.liteloader.launch.LiteLoaderTweaker", + "libraries":[ + { + "name":"net.minecraft:launchwrapper:1.5" + }, + { + "name":"net.sf.jopt-simple:jopt-simple:4.5" + }, + { + "name":"org.ow2.asm:asm-all:4.1" + } + ], + "stream":"RELEASE", + "file":"liteloader-1.5.2_01.jar", + "version":"1.5.2_01", + "md5":"53639d52340479ccf206a04f5e16606f", + "timestamp":"1367366420" + }, +''' + + +class LiteloaderArtefact(JsonObject): + tweakClass = StringProperty(required=True) + libraries = ListProperty(PolyMCLibrary, required=True) + stream = StringProperty(required=True) + file = StringProperty(required=True) + version = StringProperty(required=True) + build = StringProperty(default=None, exclude_if_none=True) + md5 = StringProperty(required=True) + timestamp = StringProperty(required=True) + srcJar = StringProperty(default=None, exclude_if_none=True) + mcpJar = StringProperty(default=None, exclude_if_none=True) + + +class LiteloaderDev(JsonObject): + fgVersion = StringProperty(default=None, exclude_if_none=True) + mappings = StringProperty(required=None, exclude_if_none=True) + mcp = StringProperty(default=None, exclude_if_none=True) + + +class LiteloaderArtefacts(JsonObject): + liteloader = DictProperty(LiteloaderArtefact, name="com.mumfrey:liteloader", required=True) + + +class LiteloaderSnapshot(LiteloaderArtefact): + lastSuccessfulBuild = IntegerProperty() + + +class LiteloaderSnapshots(JsonObject): + libraries = ListProperty(PolyMCLibrary, required=True) + liteloader = DictProperty(LiteloaderSnapshot, name="com.mumfrey:liteloader", required=True) + + +''' + "1.10.2":{ + "dev": { ... }, + "repo":{ ... }, + "artefacts":{ + "com.mumfrey:liteloader":{ }, + ... + }, + "snapshots":{ + ... + } +''' + + +class LiteloaderEntry(JsonObject): + dev = ObjectProperty(LiteloaderDev, default=None, exclude_if_none=True) + repo = ObjectProperty(LiteloaderRepo, required=True) + artefacts = ObjectProperty(LiteloaderArtefacts, default=None, exclude_if_none=True) + snapshots = ObjectProperty(LiteloaderSnapshots, default=None, exclude_if_none=True) + + +''' + "meta":{ + "description":"LiteLoader is a lightweight mod bootstrap designed to provide basic loader functionality for mods which don't need to modify game mechanics.", + "authors":"Mumfrey", + "url":"http:\/\/dl.liteloader.com", + "updated":"2017-02-22T11:34:07+00:00", + "updatedTime":1487763247 + }, +''' + + +class LiteloaderMeta(JsonObject): + description = StringProperty(required=True) + authors = StringProperty(required=True) + url = StringProperty(required=True) + updated = ISOTimestampProperty(required=True) + updatedTime = IntegerProperty(required=True) + + +# The raw Forge version index +class LiteloaderIndex(JsonObject): + meta = ObjectProperty(LiteloaderMeta, required=True) + versions = DictProperty(LiteloaderEntry) diff --git a/meta/metautil.py b/meta/metautil.py new file mode 100644 index 0000000000..e417bd6e65 --- /dev/null +++ b/meta/metautil.py @@ -0,0 +1,477 @@ +import datetime +import json +import os + +import iso8601 +from .jsonobject import * + +PMC_DIR = os.environ["PMC_DIR"] + + +class ISOTimestampProperty(AbstractDateProperty): + _type = datetime.datetime + + def _wrap(self, value): + try: + return iso8601.parse_date(value) + except ValueError as e: + raise ValueError( + 'Invalid ISO date/time {0!r} [{1}]'.format(value, e)) + + def _unwrap(self, value): + return value, value.isoformat() + + +class GradleSpecifier: + ''' + A gradle specifier - a maven coordinate. Like one of these: + "org.lwjgl.lwjgl:lwjgl:2.9.0" + "net.java.jinput:jinput:2.0.5" + "net.minecraft:launchwrapper:1.5" + ''' + + def __init__(self, name): + atSplit = name.split('@') + + components = atSplit[0].split(':') + self.group = components[0] + self.artifact = components[1] + self.version = components[2] + + self.extension = 'jar' + if len(atSplit) == 2: + self.extension = atSplit[1] + + if len(components) == 4: + self.classifier = components[3] + else: + self.classifier = None + + def toString(self): + extensionStr = '' + if self.extension != 'jar': + extensionStr = "@%s" % self.extension + if self.classifier: + return "%s:%s:%s:%s%s" % (self.group, self.artifact, self.version, self.classifier, extensionStr) + else: + return "%s:%s:%s%s" % (self.group, self.artifact, self.version, extensionStr) + + def getFilename(self): + if self.classifier: + return "%s-%s-%s.%s" % (self.artifact, self.version, self.classifier, self.extension) + else: + return "%s-%s.%s" % (self.artifact, self.version, self.extension) + + def getBase(self): + return "%s/%s/%s/" % (self.group.replace('.', '/'), self.artifact, self.version) + + def getPath(self): + return self.getBase() + self.getFilename() + + def __repr__(self): + return "GradleSpecifier('" + self.toString() + "')" + + def isLwjgl(self): + return self.group in ("org.lwjgl", "org.lwjgl.lwjgl", "net.java.jinput", "net.java.jutils") + + def isLog4j(self): + return self.group == "org.apache.logging.log4j" + + def __lt__(self, other): + return self.toString() < other.toString() + + def __eq__(self, other): + return self.group == other.group and self.artifact == other.artifact and self.version == other.version and self.classifier == other.classifier + + def __ne__(self, other): + return not self.__eq__(other) + + def __hash__(self): + return self.toString().__hash__() + + +class GradleSpecifierProperty(JsonProperty): + def wrap(self, value): + return GradleSpecifier(value) + + def unwrap(self, value): + return value, value.toString() + + +''' +Mojang index files look like this: +{ + "latest": { + "release": "1.11.2", + "snapshot": "17w06a" + }, + "versions": [ + ... + { + "id": "17w06a", + "releaseTime": "2017-02-08T13:16:29+00:00", + "time": "2017-02-08T13:17:20+00:00", + "type": "snapshot", + "url": "https://launchermeta.mojang.com/mc/game/7db0c61afa278d016cf1dae2fba0146edfbf2f8e/17w06a.json" + }, + ... + ] +} +''' + + +class MojangIndexEntry(JsonObject): + id = StringProperty() + releaseTime = ISOTimestampProperty() + time = ISOTimestampProperty() + type = StringProperty() + url = StringProperty() + sha1 = StringProperty(exclude_if_none=True, default=None) + complianceLevel = IntegerProperty(exclude_if_none=True, default=None) + + +class MojangIndex(JsonObject): + latest = DictProperty(StringProperty) + versions = ListProperty(MojangIndexEntry) + + +class MojangIndexWrap: + def __init__(self, json): + self.index = MojangIndex.wrap(json) + self.latest = self.index.latest + versionsDict = {} + for version in self.index.versions: + versionsDict[version.id] = version + self.versions = versionsDict + + +class MojangArtifactBase(JsonObject): + sha1 = StringProperty(exclude_if_none=True, default=None) + size = IntegerProperty(exclude_if_none=True, default=None) + url = StringProperty() + + +class MojangArtifact(MojangArtifactBase): + path = StringProperty(exclude_if_none=True, default=None) + + +class MojangAssets(MojangArtifactBase): + id = StringProperty() + totalSize = IntegerProperty() + + +class MojangLibraryDownloads(JsonObject): + artifact = ObjectProperty(MojangArtifact, exclude_if_none=True, default=None) + classifiers = DictProperty(MojangArtifact, exclude_if_none=True, default=None) + + +class MojangLibraryExtractRules(JsonObject): + exclude = ListProperty(StringProperty) + + +''' + "rules": [ + { + "action": "allow" + }, + { + "action": "disallow", + "os": { + "name": "osx" + } + } + ] +''' + + +class OSRule(JsonObject): + name = StringProperty(choices=["osx", "linux", "windows"], required=True) + version = StringProperty(exclude_if_none=True, default=None) + + +class MojangRule(JsonObject): + action = StringProperty(choices=["allow", "disallow"], required=True) + os = ObjectProperty(OSRule, exclude_if_none=True, default=None) + + +class MojangLibrary(JsonObject): + extract = ObjectProperty(MojangLibraryExtractRules, exclude_if_none=True, default=None) + name = GradleSpecifierProperty(required=True) + downloads = ObjectProperty(MojangLibraryDownloads, exclude_if_none=True, default=None) + natives = DictProperty(StringProperty, exclude_if_none=True, default=None) + rules = ListProperty(MojangRule, exclude_if_none=True, default=None) + + +class MojangLoggingArtifact(MojangArtifactBase): + id = StringProperty() + + +class MojangLogging(JsonObject): + file = ObjectProperty(MojangLoggingArtifact, required=True) + argument = StringProperty(required=True) + type = StringProperty(required=True, choices=["log4j2-xml"]) + + +class MojangArguments(JsonObject): + game = ListProperty(exclude_if_none=True, default=None) + jvm = ListProperty(exclude_if_none=True, default=None) + + +class JavaVersion(JsonObject): + component = StringProperty(default="jre-legacy") + majorVersion = IntegerProperty(default=8) + + +class UnknownVersionException(Exception): + """Exception raised for unknown Mojang version file format versions. + + Attributes: + message -- explanation of the error + """ + + def __init__(self, message): + self.message = message + + +def validateSupportedMojangVersion(version): + supportedVersion = 21 + if version > supportedVersion: + raise UnknownVersionException( + "Unsupported Mojang format version: %d. Max supported is: %d" % (version, supportedVersion)) + + +class MojangVersionFile(JsonObject): + arguments = ObjectProperty(MojangArguments, exclude_if_none=True, default=None) + assetIndex = ObjectProperty(MojangAssets, exclude_if_none=True, default=None) + assets = StringProperty(exclude_if_none=True, default=None) + downloads = DictProperty(MojangArtifactBase, exclude_if_none=True, default=None) + id = StringProperty(exclude_if_none=True, default=None) + libraries = ListProperty(MojangLibrary, exclude_if_none=True, default=None) + mainClass = StringProperty(exclude_if_none=True, default=None) + processArguments = StringProperty(exclude_if_none=True, default=None) + minecraftArguments = StringProperty(exclude_if_none=True, default=None) + minimumLauncherVersion = IntegerProperty(exclude_if_none=True, default=None, + validators=validateSupportedMojangVersion) + releaseTime = ISOTimestampProperty(exclude_if_none=True, default=None) + time = ISOTimestampProperty(exclude_if_none=True, default=None) + type = StringProperty(exclude_if_none=True, default=None) + inheritsFrom = StringProperty(exclude_if_none=True, default=None) + logging = DictProperty(MojangLogging, exclude_if_none=True, default=None) + complianceLevel = IntegerProperty(exclude_if_none=True, default=None) + javaVersion = ObjectProperty(JavaVersion, exclude_if_none=True, default=None) + + +CurrentPolyMCFormatVersion = 1 + + +def validateSupportedPolyMCVersion(version): + if version > CurrentPolyMCFormatVersion: + raise UnknownVersionException( + "Unsupported PolyMC format version: %d. Max supported is: %d" % (version, CurrentPolyMCFormatVersion)) + + +class PolyMCLibrary(MojangLibrary): + url = StringProperty(exclude_if_none=True, default=None) + mmcHint = StringProperty(name="MMC-hint", exclude_if_none=True, default=None) # this is supposed to be MMC-hint! + + +class VersionedJsonObject(JsonObject): + formatVersion = IntegerProperty(default=CurrentPolyMCFormatVersion, validators=validateSupportedPolyMCVersion) + + +class DependencyEntry(JsonObject): + uid = StringProperty(required=True) + equals = StringProperty(exclude_if_none=True, default=None) + suggests = StringProperty(exclude_if_none=True, default=None) + + +class PolyMCVersionFile(VersionedJsonObject): + name = StringProperty(required=True) + version = StringProperty(required=True) + uid = StringProperty(required=True) + requires = ListProperty(DependencyEntry, exclude_if_none=True, default=None) + conflicts = ListProperty(DependencyEntry, exclude_if_none=True, default=None) + volatile = BooleanProperty(exclude_if_none=True, default=None) + assetIndex = ObjectProperty(MojangAssets, exclude_if_none=True, default=None) + libraries = ListProperty(PolyMCLibrary, exclude_if_none=True, default=None) + mavenFiles = ListProperty(PolyMCLibrary, exclude_if_none=True, default=None) + mainJar = ObjectProperty(PolyMCLibrary, exclude_if_none=True, default=None) + jarMods = ListProperty(PolyMCLibrary, exclude_if_none=True, default=None) + mainClass = StringProperty(exclude_if_none=True, default=None) + appletClass = StringProperty(exclude_if_none=True, default=None) + minecraftArguments = StringProperty(exclude_if_none=True, default=None) + releaseTime = ISOTimestampProperty(exclude_if_none=True, default=None) + type = StringProperty(exclude_if_none=True, default=None) + compatibleJavaMajors = ListProperty(int, exclude_if_none=True, default=None) + addTraits = ListProperty(StringProperty, name="+traits", exclude_if_none=True, default=None) + addTweakers = ListProperty(StringProperty, name="+tweakers", exclude_if_none=True, default=None) + order = IntegerProperty(exclude_if_none=True, default=None) + + +class UnknownComplianceLevelException(Exception): + """Exception raised for unknown Mojang compliance level + + Attributes: + message -- explanation of the error + """ + + def __init__(self, message): + self.message = message + + +# Convert Mojang version file object to a PolyMC version file object +def MojangToPolyMC(file, name, uid, version): + pmcFile = PolyMCVersionFile( + { + "name": name, + "uid": uid, + "version": version + } + ) + pmcFile.assetIndex = file.assetIndex + pmcFile.libraries = file.libraries + pmcFile.mainClass = file.mainClass + if file.id: + mainJar = PolyMCLibrary( + { + "name": "com.mojang:minecraft:%s:client" % file.id, + } + ) + cldl = file.downloads['client'] + mainJar.downloads = MojangLibraryDownloads() + mainJar.downloads.artifact = MojangArtifact() + mainJar.downloads.artifact.path = None + mainJar.downloads.artifact.url = cldl.url + mainJar.downloads.artifact.sha1 = cldl.sha1 + mainJar.downloads.artifact.size = cldl.size + pmcFile.mainJar = mainJar + + pmcFile.minecraftArguments = file.minecraftArguments + pmcFile.releaseTime = file.releaseTime + # time should not be set. + pmcFile.type = file.type + + if file.javaVersion is not None: # some versions don't have this. TODO: maybe maintain manual overrides + major = file.javaVersion.majorVersion + pmcFile.compatibleJavaMajors = [major] + if major == 16: # TODO: deal with this somewhere else + pmcFile.compatibleJavaMajors.append(17) + + maxSupportedLevel = 1 + if file.complianceLevel: + if file.complianceLevel == 0: + pass + elif file.complianceLevel == 1: + if not pmcFile.addTraits: + pmcFile.addTraits = [] + pmcFile.addTraits.append("XR:Initial") + else: + raise UnknownComplianceLevelException("Unsupported Mojang compliance level: %d. Max supported is: %d" % ( + file.complianceLevel, maxSupportedLevel)) + return pmcFile + + +class PolyMCSharedPackageData(VersionedJsonObject): + name = StringProperty(required=True) + uid = StringProperty(required=True) + recommended = ListProperty(StringProperty, exclude_if_none=True, default=None) + authors = ListProperty(StringProperty, exclude_if_none=True, default=None) + description = StringProperty(exclude_if_none=True, default=None) + projectUrl = StringProperty(exclude_if_none=True, default=None) + + def write(self): + try: + with open(PMC_DIR + "/%s/package.json" % self.uid, 'w') as file: + json.dump(self.to_json(), file, sort_keys=True, indent=4) + except EnvironmentError as e: + print("Error while trying to save shared packaged data for %s:" % self.uid, e) + + +def writeSharedPackageData(uid, name): + desc = PolyMCSharedPackageData({ + 'name': name, + 'uid': uid + }) + with open(PMC_DIR + "/%s/package.json" % uid, 'w') as file: + json.dump(desc.to_json(), file, sort_keys=True, indent=4) + + +def readSharedPackageData(uid): + with open(PMC_DIR + "/%s/package.json" % uid, 'r') as file: + return PolyMCSharedPackageData(json.load(file)) + + +class PolyMCVersionIndexEntry(JsonObject): + version = StringProperty() + type = StringProperty(exclude_if_none=True, default=None) + releaseTime = ISOTimestampProperty() + requires = ListProperty(DependencyEntry, exclude_if_none=True, default=None) + conflicts = ListProperty(DependencyEntry, exclude_if_none=True, default=None) + recommended = BooleanProperty(exclude_if_none=True, default=None) + volatile = BooleanProperty(exclude_if_none=True, default=None) + sha256 = StringProperty() + + +class PolyMCVersionIndex(VersionedJsonObject): + name = StringProperty() + uid = StringProperty() + versions = ListProperty(PolyMCVersionIndexEntry) + + +class PolyMCPackageIndexEntry(JsonObject): + name = StringProperty() + uid = StringProperty() + sha256 = StringProperty() + + +class PolyMCPackageIndex(VersionedJsonObject): + packages = ListProperty(PolyMCPackageIndexEntry) + + +''' +The PolyMC static override file for legacy looks like this: +{ + "versions": [ + ... + { + "id": "c0.0.13a", + "checksum": "3617fbf5fbfd2b837ebf5ceb63584908", + "releaseTime": "2009-05-31T00:00:00+02:00", + "type": "old_alpha", + "mainClass": "com.mojang.minecraft.Minecraft", + "appletClass": "com.mojang.minecraft.MinecraftApplet", + "+traits": ["legacyLaunch", "no-texturepacks"] + }, + ... + ] +} +''' + + +class LegacyOverrideEntry(JsonObject): + releaseTime = ISOTimestampProperty(exclude_if_none=True, default=None) + mainClass = StringProperty(exclude_if_none=True, default=None) + appletClass = StringProperty(exclude_if_none=True, default=None) + addTraits = ListProperty(StringProperty, name="+traits", exclude_if_none=True, default=None) + + +class LegacyOverrideIndex(JsonObject): + versions = DictProperty(LegacyOverrideEntry) + + +def ApplyLegacyOverride(pmcFile, legacyOverride): + # simply hard override classes + pmcFile.mainClass = legacyOverride.mainClass + pmcFile.appletClass = legacyOverride.appletClass + # if we have an updated release time (more correct than Mojang), use it + if legacyOverride.releaseTime != None: + pmcFile.releaseTime = legacyOverride.releaseTime + # add traits, if any + if legacyOverride.addTraits: + if not pmcFile.addTraits: + pmcFile.addTraits = [] + pmcFile.addTraits = pmcFile.addTraits + legacyOverride.addTraits + # remove all libraries - they are not needed for legacy + pmcFile.libraries = None + # remove minecraft arguments - we use our own hardcoded ones + pmcFile.minecraftArguments = None |
