Minecraft-Overviewer/overviewer_core/settingsValidators.py

467 lines
16 KiB
Python

# see settingsDefinition.py
import logging
import os
import os.path
from collections import OrderedDict
from . import rendermodes
from . import util
from .optimizeimages import Optimizer
from .world import LOWER_LEFT, LOWER_RIGHT, UPPER_LEFT, UPPER_RIGHT
class ValidationException(Exception):
pass
class Setting(object):
__slots__ = ['required', 'validator', 'default']
def __init__(self, required, validator, default):
self.required = required
self.validator = validator
self.default = default
def expand_path(p):
p = os.path.expanduser(p)
p = os.path.expandvars(p)
p = os.path.abspath(p)
return p
def checkBadEscape(s):
# If any of these weird characters are in the path, raise an exception
# instead of fixing this should help us educate our users about pathslashes
bad_escapes = ['\a', '\b', '\t', '\n', '\v', '\f', '\r']
for b in bad_escapes:
if b in s:
raise ValueError("Invalid character %s in path. Please use "
"forward slashes ('/'). Please see our docs for "
"more info." % repr(b))
for c in range(10):
if chr(c) in s:
raise ValueError("Invalid character '\\%s' in path. "
"Please use forward slashes ('/'). "
"See our docs for more info." % c)
return s
def validateMarkers(filterlist):
if type(filterlist) != list:
raise ValidationException("Markers must specify a list of filters.")
for x in filterlist:
if type(x) != dict:
raise ValidationException("Markers must specify a list of dictionaries.")
if "name" not in x:
raise ValidationException("Filter must define a name.")
if "filterFunction" not in x:
raise ValidationException("Filter must define a filter function.")
if not callable(x['filterFunction']):
raise ValidationException("%r must be a function." % x['filterFunction'])
return filterlist
def validateOverlays(renderlist):
if type(renderlist) != list:
raise ValidationException("Overlay must specify a list of renders.")
for x in renderlist:
if validateStr(x) == '':
raise ValidationException("%r must be a string." % x)
return renderlist
def validateWorldPath(worldpath):
checkBadEscape(worldpath)
abs_path = expand_path(worldpath)
if not os.path.exists(os.path.join(abs_path, "level.dat")):
raise ValidationException("No level.dat file in '%s'. Are you sure you have the right "
"path?" % (abs_path,))
return abs_path
def validateRenderMode(mode):
# make sure that mode is a list of things that are all rendermode primative
if isinstance(mode, str):
# Try and find an item named "mode" in the rendermodes module
mode = mode.lower().replace("-", "_")
try:
mode = getattr(rendermodes, mode)
except AttributeError:
raise ValidationException("You must specify a valid rendermode, not '%s'. "
"See the docs for valid rendermodes." % mode)
if isinstance(mode, rendermodes.RenderPrimitive):
mode = [mode]
if not isinstance(mode, list):
raise ValidationException("%r is not a valid list of rendermodes. "
"It should be a list." % mode)
for m in mode:
if not isinstance(m, rendermodes.RenderPrimitive):
raise ValidationException("%r is not a valid rendermode primitive." % m)
return mode
def validateNorthDirection(direction):
# normalize to integers
intdir = 0 # default
if type(direction) == int:
intdir = direction
elif isinstance(direction, str):
direction = direction.lower().replace("-", "").replace("_", "")
if direction == "upperleft":
intdir = UPPER_LEFT
elif direction == "upperright":
intdir = UPPER_RIGHT
elif direction == "lowerright":
intdir = LOWER_RIGHT
elif direction == "lowerleft":
intdir = LOWER_LEFT
else:
raise ValidationException("'%s' is not a valid north direction." % direction)
if intdir < 0 or intdir > 3:
raise ValidationException("%r is not a valid north direction." % direction)
return intdir
def validateRerenderprob(s):
val = float(s)
if val < 0 or val >= 1:
raise ValidationException("%r is not a valid rerender probability value. "
"Should be between 0.0 and 1.0." % s)
return val
def validateImgFormat(fmt):
if fmt not in ("png", "jpg", "jpeg", "webp"):
raise ValidationException("%r is not a valid image format." % fmt)
if fmt == "jpeg":
fmt = "jpg"
if fmt == "webp":
try:
from PIL import _webp
except ImportError:
raise ValidationException("WebP is not supported by your PIL/Pillow installation.")
return fmt
def validateImgQuality(qual):
intqual = int(qual)
if (intqual < 0 or intqual > 100):
raise ValidationException("%r is not a valid image quality." % intqual)
return intqual
def validateBGColor(color):
"""BG color must be an HTML color, with an option leading # (hash symbol)
returns an (r,b,g) 3-tuple
"""
if type(color) == str:
if color[0] != "#":
color = "#" + color
if len(color) != 7:
raise ValidationException("%r is not a valid color. Expected HTML color syntax. "
"(i.e. #RRGGBB)" % color)
try:
r = int(color[1:3], 16)
g = int(color[3:5], 16)
b = int(color[5:7], 16)
return (r, g, b, 0)
except ValueError:
raise ValidationException("%r is not a valid color. Expected HTML color syntax. "
"(i.e. #RRGGBB)" % color)
elif type(color) == tuple:
if len(color) != 4:
raise ValidationException("%r is not a valid color. Expected a 4-tuple." % (color,))
return color
def validateOptImg(optimizers):
if isinstance(optimizers, int):
from .optimizeimages import pngcrush
logging.warning("You're using a deprecated definition of optimizeimg. "
"We'll do what you say for now, but please fix this as soon as possible.")
optimizers = [pngcrush()]
if not isinstance(optimizers, list):
raise ValidationException("What you passed to optimizeimg is not a list. "
"Make sure you specify them like [foo()], with square brackets.")
if optimizers:
for opt, next_opt in list(zip(optimizers, optimizers[1:])) + [(optimizers[-1], None)]:
if not isinstance(opt, Optimizer):
raise ValidationException("Invalid Optimizer!")
opt.check_availability()
# Check whether the chaining is somewhat sane
if next_opt:
if opt.is_crusher() and not next_opt.is_crusher():
logging.warning("You're feeding a crushed output into an optimizer that "
"does not crush. This is most likely pointless, and wastes "
"time.")
return optimizers
def validateTexturePath(path):
# Expand user dir in directories strings
path = expand_path(path)
if not os.path.exists(path):
raise ValidationException("%r does not exist." % path)
return path
def validateBool(b):
return bool(b)
def validateFloat(f):
return float(f)
def validateInt(i):
return int(i)
def validateStr(s):
return str(s)
def validateDimension(d):
# returns (original, argument to get_type)
# these are provided as arguments to RegionSet.get_type()
pretty_names = {
"nether": "DIM-1",
"overworld": None,
"end": "DIM1",
"default": 0,
}
try:
return (d, pretty_names[d])
except KeyError:
return (d, d)
def validateOutputDir(d):
checkBadEscape(d)
if not d.strip():
raise ValidationException("You must specify a valid output directory.")
return expand_path(d)
def validateCrop(value):
if not isinstance(value, list):
value = [value]
cropZones = []
for zone in value:
if not isinstance(zone, tuple) or len(zone) != 4:
raise ValidationException("The value for the 'crop' setting must be an array of "
"tuples of length 4.")
a, b, c, d = tuple(int(x) for x in zone)
if a >= c:
a, c = c, a
if b >= d:
b, d = d, b
cropZones.append((a, b, c, d))
return cropZones
def validateObserver(observer):
if all([hasattr(observer, m) for m in ['start', 'add', 'update', 'finish']]):
return observer
else:
raise ValidationException("%r does not look like an observer." % repr(observer))
def validateDefaultZoom(z):
if z > 0:
return int(z)
else:
raise ValidationException("The default zoom is set below 1.")
def validateWebAssetsPath(p):
try:
validatePath(p)
except ValidationException as e:
raise ValidationException("Bad custom web assets path: %s" % e.message)
def validatePath(p):
checkBadEscape(p)
abs_path = expand_path(p)
if not os.path.exists(abs_path):
raise ValidationException("'%s' does not exist. Path initially given as '%s'"
% (abs_path, p))
def validateManualPOIs(d):
for poi in d:
if 'x' not in poi or 'y' not in poi or 'z' not in poi or 'id' not in poi:
raise ValidationException("Not all POIs have x/y/z coordinates or an id: %r." % poi)
return d
def validateCoords(c):
if not isinstance(c, (list, tuple)):
raise ValidationException("Your coordinates '{}' are not a list or a tuple.".format(c))
if len(c) not in [2, 3]:
raise ValidationException("'{}' is not a valid list or tuple of coordinates, "
"because we expect either 2 or 3 elements.".format(c))
if len(c) == 2:
x, z = [validateInt(i) for i in c]
y = 64
else:
x, y, z = [validateInt(i) for i in c]
return (x, y, z)
def make_dictValidator(keyvalidator, valuevalidator):
"""Compose and return a dict validator -- a validator that validates each
key and value in a dictionary.
The arguments are the validator function to use for the keys, and the
validator function to use for the values.
"""
def v(d):
newd = OrderedDict()
for key, value in d.items():
newd[keyvalidator(key)] = valuevalidator(value)
return newd
# Put these objects as attributes of the function so they can be accessed
# externally later if need be
v.keyvalidator = keyvalidator
v.valuevalidator = valuevalidator
return v
def make_configDictValidator(config, ignore_undefined=False):
"""Okay, stay with me here, this may get confusing. This function returns a
validator that validates a "configdict". This is a term I just made up to
refer to a dict that holds config information: keys are strings and values
are whatever that config value is. The argument to /this/ function is a
dictionary defining the valid keys for the configdict. It therefore maps
those key names to Setting objects. When the returned validator function
validates, it calls all the appropriate validators for each configdict
setting
I hope that makes sense.
ignore_undefined, if True, will ignore any items in the dict to be
validated which don't have a corresponding definition in the config.
Otherwise, undefined entries will raise an error.
"""
def configDictValidator(d):
newdict = OrderedDict()
# values are config keys that the user specified that don't match any
# valid key
# keys are the correct configuration key
undefined_key_matches = {}
# Go through the keys the user gave us and make sure they're all valid.
for key in d.keys():
if key not in config:
# Try to find a probable match
match = _get_closest_match(key, iter(config.keys()))
if match and ignore_undefined:
# Save this for later. It only matters if this is a typo of
# some required key that wasn't specified. (If all required
# keys are specified, then this should be ignored)
undefined_key_matches[match] = key
newdict[key] = d[key]
elif match:
raise ValidationException(
"'%s' is not a configuration item. Did you mean '%s'?" % (key, match))
elif not ignore_undefined:
raise ValidationException("'%s' is not a configuration item." % key)
else:
# the key is to be ignored. Copy it as-is to the `newdict`
# to be returned. It shouldn't conflict, and may be used as
# a default value for a render configdict later on.
newdict[key] = d[key]
# Iterate through the defined keys in the configuration (`config`),
# checking each one to see if the user specified it (in `d`). Then
# validate it and copy the result to `newdict`
for configkey, configsetting in config.items():
if configkey in d:
# This key /was/ specified in the user's dict. Make sure it validates.
newdict[configkey] = configsetting.validator(d[configkey])
elif configsetting.default is not None:
# There is a default, use that instead
newdict[configkey] = configsetting.validator(configsetting.default)
elif configsetting.required:
# The user did not give us this key, there is no default, AND
# it's required. This is an error.
if configkey in undefined_key_matches:
raise ValidationException(
"Key '%s' is not a valid configuration item. Did you mean '%s'?"
% (undefined_key_matches[configkey], configkey))
else:
raise ValidationException("Required key '%s' was not specified. You must give "
"a value for this setting." % configkey)
return newdict
# Put these objects as attributes of the function so they can be accessed
# externally later if need be
configDictValidator.config = config
configDictValidator.ignore_undefined = ignore_undefined
return configDictValidator
def error(errstr):
def validator(_):
raise ValidationException(errstr)
return validator
# Activestate recipe 576874
def _levenshtein(s1, s2):
l1 = len(s1)
l2 = len(s2)
matrix = [list(range(l1 + 1))] * (l2 + 1)
for zz in range(l2 + 1):
matrix[zz] = list(range(zz, zz + l1 + 1))
for zz in range(0, l2):
for sz in range(0, l1):
if s1[sz] == s2[zz]:
matrix[zz + 1][sz + 1] = min(matrix[zz + 1][sz] + 1, matrix[zz][sz + 1] + 1,
matrix[zz][sz])
else:
matrix[zz + 1][sz + 1] = min(matrix[zz + 1][sz] + 1, matrix[zz][sz + 1] + 1,
matrix[zz][sz] + 1)
return matrix[l2][l1]
def _get_closest_match(s, keys):
"""Returns a probable match for the given key `s` out of the possible keys in
`keys`. Returns None if no matches are very close.
"""
# Adjust this. 3 is probably a good number, it's probably not a typo if the
# distance is >3
threshold = 3
minmatch = None
mindist = threshold + 1
for key in keys:
d = _levenshtein(s, key)
if d < mindist:
minmatch = key
mindist = d
if mindist <= threshold:
return minmatch
return None