"""
This module contains functions that are used as validators for parameters. Each
is decorated with the @validator decorator, which accepts an arbitrary number
of examples that should correctly validate.
For example, here's a validator that would be used to ensure a string has
exactly one string character:
@validator("a", "1")
def one_char(v):
return isinstance(v, string_types) and len(v) == 1
"""
import warnings
from .compatibility import string_types
from . import settings
class ValidationError(Exception):
pass
def validator(*example):
"""
Decorator that runs a self-test on the validator it decorates
"""
def wrapper(func):
# Try running validation on the function's own example . . . it better
# work!
example_string = []
try:
for ex in example:
func(ex)
example_string.append(ex)
example_string = "Example value(s): %s" % (
" or ".join("%r" % i for i in example_string)
)
except Exception as e:
raise ValueError(
"Error validating example (func=%s, example=%r)! "
"\nOriginal error:\n\t%s: %s"
% (func.__name__, example, e.__class__.__name__, e.message)
)
class Validator(object):
"""
Class to wrap a function and display an example value.
"""
_func = func
def __call__(self, v):
try:
result = func(v)
except Exception as e:
result = False
if not result and settings.VALIDATE:
raise ValidationError(
"Value {0} failed {1} validation; {2}".format(
v, func.__name__, example_string
)
)
return result
def __str__(self):
return "<Validator [%s] at %s> sample: %s" % (
func.__name__,
id(func),
example,
)
return Validator()
return wrapper
[docs]
class Param(object):
[docs]
def __init__(self, name, fmt, types, required, validator, min_bed_fields=None):
"""
Parameters
----------
name : str
Name of the parameter
fmt : list
List of strings parsed from the "format" section of the spec from
UCSC. Mostly used as an informal guide to the format.
types : list
List of track types this parameter applies to
required : bool or list
If True, all tracks must have it. If list, only those types must
have it.
validator : callable, set, or type
Validation to run on user-provided values. If callable, must return
True if the value passes. If set, validation will pass if the value
is in the provided set.
min_bed_fields : int
Some parameters only work for a certain number of BED fields.
Specify that here.
Examples
--------
>>> Param(name='test', fmt=['test <#>'], types=['bigBed'], required=False, validator=int).validate(999)
True
>>> Param(name='test', fmt=['test <#>'], types=['bigBed'], required=False, validator=int).validate('999')
True
>>> Param(name='test', fmt=['test <#>'], types=['bigBed'], required=False, validator=int).validate(0)
True
"""
self.name = name
self.fmt = fmt
self.types = types
self.required = required
self.validator = validator
def __str__(self):
return '<%s "%s" at %s>' % (self.__class__.__name__, self.name, id(self))
def validate(self, value):
if isinstance(self.validator, set):
if value in self.validator:
return True
if isinstance(self.validator, type):
if isinstance(value, self.validator):
return True
else:
# Otherwise, allow any exceptions to propagate up.
self.validator(value)
return True
if hasattr(self.validator, "__call__"):
return self.validator(value)
elif value == self.validator:
return True
return False
@validator("tag=value", "tag1=val1 tag2=val2")
def key_val(v):
try:
assert "=" in v
items = v.split()
assert len(items) == v.count("=")
return True
except AssertionError:
raise ValidationError
@validator("a,b,c")
def CSV(v):
# TODO: is a one-item list "chr1," or "chr1"?
if isinstance(v, string_types):
return True
raise ValidationError
@validator("a:b:c", "0:10:100")
def ColSV3(v):
nvalues = 3
if not isinstance(v, string_types):
raise ValueError("not a string")
vs = v.split(":")
assert len(vs) == nvalues
return True
@validator("a:b")
def ColSV2(v):
nvalues = 2
if not isinstance(v, string_types):
raise ValueError("not a string")
vs = v.split(":")
assert len(vs) == nvalues
return True
@validator("1:5", 3, "3")
def ColSV2_numbers_or_single_number(v):
v = str(v)
vs = v.split(":")
assert len(vs) in [1, 2]
for i in vs:
try:
float(i)
except ValueError:
raise ValueError(
"{0} in parameter {1} cannot be converted " "into a number".format(i, v)
)
return True
@validator("128,0,255")
def RGB(v):
if " " in v:
raise ValueError("Space in RGB tuple")
if "." in v:
raise ValueError('"." in RGB tuple')
assert isinstance(v, string_types), "RGB tuple is not a string"
if "," not in v:
raise ValueError("no commas in RGB tuple")
rgb = v.split(",")
assert len(rgb) == 3, "RGB tuple does not have 3 values"
try:
rgb = map(int, rgb)
except ValueError:
raise ValueError("RGB tuple does not contain ints")
for i in rgb:
assert 0 <= i <= 255
return True
@validator("128,0,0 90,90,5")
def RGBList(v):
rgbs = v.split(" ")
assert len(rgbs) == 2, "RGBList not a space-separated list of RGB tuples"
for i in rgbs:
RGB(i)
return True
@validator("off", 1)
def off_or_int(v):
try:
int(v)
return True
except ValueError:
if v == "off":
return True
raise
@validator("chr21:33031596-33033258")
def ucsc_position(v):
try:
chrom, pos = v.split(":")
start, end = pos.split("-")
start = int(start)
end = int(end)
except ValueError:
raise ValueError("UCSC position string is formatted incorrectly")
assert start >= 0, "start position must be a positive integer"
assert end >= 0, "end position must be a positive integer"
assert start < end, "start must be less than end"
return True
@validator("asdf1234_33", "AZ90")
def alphanumeric_(v):
valid = "abcdefghijklmnopqrstuvwxyz"
valid += "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
valid += "01234556789"
valid += "_"
assert isinstance(v, string_types)
for i in v:
if i not in valid:
return False
return True
@validator("one two three", "aaaaaaaaaaaaaaaaa")
def short_label(v):
assert isinstance(v, string_types)
if len(v) > 17:
warnings.warn(
"shortLabel is limited to 17 characters "
"in the browser, some characters will be truncated"
)
return True
@validator("a" * 76, "four five six")
def long_label(v):
assert isinstance(v, string_types)
if len(v) > 76:
warnings.warn(
"longLabel is limited to 76 characters "
"in the browser, some characters will be truncated"
)
return True
@validator("https://example.com", "path/to/a.html")
def full_or_local_url(v):
return isinstance(v, string_types)
@validator("https://example.com", "path/to/a.html")
def full_url(v):
return isinstance(v, string_types)
@validator(1, "1", "500")
def int_like(v):
try:
int(v)
return True
except ValueError:
return False
@validator(1.0, "5.556")
def float_like(v):
try:
float(v)
return True
except ValueError:
return False
@validator("#ff0000", "maroon")
def hex_or_named(v):
valid = "0123456789ABCDEF"
try:
if v.startswith("#"):
assert len(v.upper()[1:]) == 6
for i in v[1:]:
assert i in valid
else:
assert v in set(
[
"black",
"silver",
"gray",
"white",
"maroon",
"red",
"purple",
"fuchsia",
"green",
"lime",
"olive",
"yellow",
"navy",
"blue",
"teal",
"aqua",
]
)
except AssertionError:
return False
return True
@validator("bigBed", "bigBed 6+3")
def tracktypes(v):
return True