import re
from pygfa.graph_element.parser import field_validator as fv
[docs]class InvalidLineError(Exception):
"""Exception raised when making a Line object from a string.
The number of fields gained by splittin the string
must be equal to or great than the number of required field
ecluding the optional first field indicating the type of the line.
"""
# support for duck typing
[docs]def is_field(field):
"""Check if the given object is a valid field
A field is valid if it has at least a name and a value
attribute/property.
"""
for attr in('name', 'value'):
if not hasattr(field, attr):
return False
if field.name is None or field.value is None:
return False
if not isinstance(field.name, str):
return False
return True
[docs]def is_optfield(field):
"""Check if the given object is an optfield
A field is an optfield if it's a field with name that
match a given expression and its type is defined.
"""
return is_field(field) and \
re.fullmatch('[A-Za-z0-9]' * 2, field.name) and \
hasattr(field, '_type') and \
field.type != None
[docs]class Line:
"""
A generic Line, it's unlikely that it will be directly instantiated
(but could be done so).
Its subclasses should be used instead.
It's possible to instatiate a Line to save a custom line in a gfa file.
"""
REQUIRED_FIELDS = {}
PREDEFINED_OPTFIELDS = {}
# this will contain tha name of the required optfield for
# each kind of line and the ralative type of value the value of
# the field must contains
def __init__(self, line_type=None):
self._fields = {}
self._type = line_type
@classmethod
[docs] def is_valid(cls, line_):
"""Check if the line is valid.
Defining the method here allows to have automatically validated
all the line of the specifications.
"""
# use polymorphism to get the type and the required fields of a
# specific kind of line.
instance = cls()
try:
if line_.type != cls().type:
return False
for required_field in instance.REQUIRED_FIELDS:
if not required_field in line_.fields:
return False
return True
except (AttributeError, KeyError):
return False
@classmethod
[docs] def get_static_fields(cls):
keys = []
values = []
for key, value in cls.REQUIRED_FIELDS.items():
keys.append(key)
values.append(value)
for key, value in cls.PREDEFINED_OPTFIELDS.items():
keys.append(key)
values.append(value)
return dict(zip(keys, values))
@property
def type(self):
return self._type
@property
def fields(self):
return self._fields
[docs] def add_field(self, field):
"""Add a field to the line.
It's possible to add a Field if an only if its name
is in the `REQUIRED_FIELDS` dictionary. Otherwise
the field will be considered as an optional field and
an InvalidFieldError will be raised.
:param field: The field to add to the line
:raises InvalidFieldError: If a 'name' and a 'value' attributes are
not found or the field has already been added.
:note:
If you want to add a Field for a custom Line object be
sure to add its name to the REQUIRED_FIELDS dictionary
for that particular Line subclass.
"""
if not(is_field(field) or is_optfield(field)):
raise fv.InvalidFieldError("A valid field must be attached")
if field.name in self.fields:
raise ValueError(\
"This field is already been added, field name: '{0}'.".format(field.name))
if field.name in self.REQUIRED_FIELDS:
self._fields[field.name] = field
else: # here we are appending an optfield
if not is_optfield(field):
raise fv.InvalidFieldError(\
"Cannot add an invalid OptField.")
self._fields[field.name] = field
return True
[docs] def remove_field(self, field):
"""
If the field is contained in the line it gets removed.
Otherwise it does nothing, without raising any exception.
"""
field_name = field
if is_field(field):
field_name = field.name
if field_name in self.fields:
self.fields.pop(field_name)
@classmethod
[docs] def from_string(cls, string): # pragma: no cover
raise NotImplementedError
def __eq__(self, other):
try:
return self.type == other.type \
and self.fields == other.fields
except:
return False
def __neq__(self, other):
return not self == other
def __str__(self): # pragma: no cover
tmp_str = "line_type: {0}, fields: [".format(str(self.type))
field_strings = []
for field in self.fields:
field_strings.append(str(field))
tmp_str += str.join(", ", field_strings) + "]"
return tmp_str
[docs]class Field:
"""This class represent any required field.
The type of field is bound to the field name.
"""
def __init__(self, name, value):
self._name = name
self._value = value
@property
def name(self):
return self._name
@property
def value(self):
return self._value
def __eq__(self, other):
try:
return self.name == other.name and \
self.value == other.value
except:
return False
def __neq__(self, other):
return not self == other
def __str__(self): # pragma: no cover
return str.join(":", (self.name, str(self.value)))
[docs]class OptField(Field):
"""An Optional field of the form `TAG:TYPE:VALUE`, where:
TAG match [A-Za-z0-9][A-Za-z0-9]
TYPE match [AiZfJHB]
"""
def __init__(self, name, value, field_type):
if not re.fullmatch('[A-Za-z0-9]' * 2, name):
raise ValueError("Invalid optfield name, given '{0}'".format(name))
if not re.fullmatch("^[ABHJZif]$", field_type):
raise ValueError("Invalid type for an optional field.")
self._name = name
self._type = field_type
self._value = fv.validate(value, field_type)
@property
def type(self):
return self._type
@classmethod
[docs] def from_string(cls, string):
"""Create an OptField with a given string.
"""
groups = re.split(":", string.strip())
if len(groups) != 3:
raise ValueError(\
"OptField must have a name, a type and a value," \
+ " given{0}".format(string))
optfield = OptField(groups[0], groups[2], groups[1])
return optfield
def __eq__(self, other):
try:
return self.name == other.name and \
self.value == other.value and \
self.type == other.type
except:
return False
def __neq__(self, other):
return not self == other
def __str__(self): # pragma: no cover
return str.join(":", (self.name, self.type, str(self.value)))
if __name__ == '__main__': # pragma: no cover
pass