class PydmrsTypeError(PydmrsError, TypeError):
    pass
 class PydmrsValueError(PydmrsError, ValueError):
    pass
 class Pred(object):
    """
    A superclass for all Pred classes.
    Instances of Pred denote a completely underspecified predicate.
    """

    __slots__ = ()  # Suppress __dict__

    def __str__(self):
        """
        Returns 'predsort', the type name for predicates in Delph-in grammars
        """
        return 'predsort'

    def __repr__(self):
        """
        Returns a string representation
        """
        return 'Pred()'

    def __hash__(self):
        return hash(None)

    def __eq__(self, other):
        """
        Checks pred equality
        """
        if not isinstance(other, Pred):
            raise PydmrsTypeError()
        return type(other) is Pred

    def __ne__(self, other):
        """
        Checks pred inequality
        """
        if not isinstance(other, Pred):
            raise PydmrsTypeError()
        return type(other) is not Pred

    def __le__(self, other):
        """
        Checks pred leq comparison (Pred < GPred < RealPred)
        """
        if not isinstance(other, Pred):
            raise PydmrsTypeError()
        return True

    def __lt__(self, other):
        """
        Checks pred lt comparison (Pred < GPred < RealPred)
        """
        if not isinstance(other, Pred):
            raise PydmrsTypeError()
        return type(other) != Pred

    def __ge__(self, other):
        """
        Checks pred geq comparison (Pred < GPred < RealPred)
        """
        if not isinstance(other, Pred):
            raise PydmrsTypeError()
        return type(other) == Pred

    def __gt__(self, other):
        """
        Checks pred gt comparison (Pred < GPred < RealPred)
        """
        if not isinstance(other, Pred):
            raise PydmrsTypeError()
        return False

    def is_more_specific(self, other, hierarchy=None):
        """
        Checks whether this object is a more specific pred than the other
        """
        if not isinstance(other, Pred):
            raise PydmrsTypeError()
        if hierarchy is not None and str(self) in hierarchy.get(str(other), ()):
            return True
        return False

    def is_less_specific(self, other, hierarchy=None):
        """
        Checks whether this object is a less specific pred than the other
        """
        if not isinstance(other, Pred):
            raise PydmrsTypeError()
        if hierarchy is not None and str(other) in hierarchy.get(str(self), ()):
            return True
        return type(other) is not Pred

    @staticmethod
    def normalise_string(string):
        """
        Normalises a predicate string
        """
        # Disallow spaces
        if ' ' in string:
            raise PydmrsValueError('Predicates must not contain spaces')
        # Strip surrounding quotes and disallow other quotes
        if string[0] == '"' and string[-1] == '"':
            string = string[1:-1]
        if string[0] == "'":
            warn('Predicates with opening single-quote have been deprecated',
                 PydmrsDeprecationWarning)
            string = string[1:]
        if '"' in string:
            raise PydmrsValueError('Predicates must not contain quotes')
        # Force lower case
        if not string.islower():
            warn('Predicates must be lower-case', PydmrsWarning)
            string = string.lower()
        # Strip trailing '_rel'
        if string[-4:] == '_rel':
            string = string[:-4]

        return string

    @classmethod
    def from_string(cls, string):
        """
        Instantiates a pred from a string, normalising as necessary
        """
        normalised = cls.normalise_string(string)
        return cls.from_normalised_string(normalised)

    @staticmethod
    def from_normalised_string(string):
        """
        Instantiates a suitable type of Pred, from a string
        """
        if string[0] == '_':
            return RealPred.from_normalised_string(string)
        else:
            return GPred.from_normalised_string(string) class GPred(namedtuple('GPredNamedTuple', ('name')), Pred):
    """
    Grammar predicate, with a rel name.
    The name can be underspecified by using '?'.
    """

    __slots__ = ()  # Suppress __dict__

    def __new__(cls, name):
        """
        Create a new instance, requiring non-empty name
        """
        if not name:
            raise PydmrsValueError('a GPred must have non-empty name')
        return super().__new__(cls, name)

    def __str__(self):
        """
        Return a string
        """
        return str(self.name)

    def __repr__(self):
        """
        Return a string, as "GPred(name)"
        """
        return "GPred({})".format(repr(self.name))

    def __hash__(self):
        return super().__hash__()

    def __eq__(self, other):
        """
        Checks pred equality
        """
        if not isinstance(other, Pred):
            raise PydmrsTypeError()
        return isinstance(other, GPred) and super().__eq__(other)

    def __ne__(self, other):
        """
        Checks pred inequality
        """
        if not isinstance(other, Pred):
            raise PydmrsTypeError()
        return not isinstance(other, GPred) or super().__ne__(other)

    def __le__(self, other):
        """
        Checks pred leq comparison (Pred < GPred < RealPred)
        """
        if not isinstance(other, Pred):
            raise PydmrsTypeError()
        return isinstance(other, RealPred) or (isinstance(other, GPred) and super().__le__(other))

    def __lt__(self, other):
        """
        Checks pred lt comparison (Pred < GPred < RealPred)
        """
        if not isinstance(other, Pred):
            raise PydmrsTypeError()
        return isinstance(other, RealPred) or (isinstance(other, GPred) and super().__lt__(other))

    def __ge__(self, other):
        """
        Checks pred geq comparison (Pred < GPred < RealPred)
        """
        if not isinstance(other, Pred):
            raise PydmrsTypeError()
        return isinstance(other, Pred) or (isinstance(other, GPred) and super().__ge__(other))

    def __gt__(self, other):
        """
        Checks pred gt comparison (Pred < GPred < RealPred)
        """
        if not isinstance(other, Pred):
            raise PydmrsTypeError()
        return isinstance(other, Pred) or (isinstance(other, GPred) and super().__gt__(other))

    def is_more_specific(self, other, hierarchy=None):
        """
        Checks whether this object is a more specific pred than the other
        """
        if not isinstance(other, Pred):
            raise PydmrsTypeError()
        if hierarchy is not None and str(self) in hierarchy.get(str(other), ()):
            return True
        return type(other) is Pred or (isinstance(other, GPred) and (self.name != '?' and other.name == '?'))

    def is_less_specific(self, other, hierarchy=None):
        """
        Checks whether this object is a less specific pred than the other
        """
        if not isinstance(other, Pred):
            raise PydmrsTypeError()
        if hierarchy is not None and str(other) in hierarchy.get(str(self), ()):
            return True
        return isinstance(other, GPred) and (self.name == '?' and other.name != '?')

    @staticmethod
    def from_normalised_string(string):
        """
        Create a new instance from a normalised string.
        """
        if string[0] == '_':
            raise PydmrsValueError("GPred strings must not begin with an underscore")
        else:
            return GPred(string) class Sortinfo(MutableMapping, metaclass=SortinfoMeta):
    """
    A superclass for all Sortinfo classes.
    Instances of Sortinfo denote completely underspecified sortinfo.
    Subclasses of Sortinfo must specify __slots__ (and optionally, cvarsort)
    """
    __slots__ = ()
    cvarsort = 'i'

    # Container methods

    def __iter__(self):
        """
        Iterate through all features, including 'cvarsort'
        """
        return chain(('cvarsort',), iter(self.features))

    def __len__(self):
        """
        Return the number of features, including 'cvarsort'
        """
        return 1 + len(self.features)

    def __contains__(self, feature):
        """
        Check if a feature is present in the class
        """
        return feature == 'cvarsort' or feature in self.features

    def is_specified(self, feature):
        """
        Returns True if value of feature is not '?' or 'u'
        """
        return feature == 'cvarsort' or (feature in self.features and self[feature] not in ('u', '?', None))

    # For convenience, we can also get features which are not None

    def iter_specified(self):
        """
        Return (feature, value) pairs where value is specified and not '?' or 'u'
        """
        for feat in self.features:
            if hasattr(self, feat):
                val = self[feat]
                if val != 'u' and val != '?' and val is not None:
                    yield feat, val

    # Setters and getters
    # Allows both attribute and dictionary access
    # Features and values must all be lowercase

    def __getattribute__(self, feature):
        try:
            return super().__getattribute__(feature)
        except AttributeError:
            if feature in self.features:
                return None
            else:
                raise

    def __setattr__(self, feature, value):
        """
        Set the value of a feature, converting it to lowercase unless it's None
        """
        feature = feature.lower()
        if value is not None:
            value = value.lower()
        super().__setattr__(feature, value)

    def __delattr__(self, feature):
        """
        Set the value of a feature to None
        """
        setattr(self, feature, None)

    def __setitem__(self, feature, value):
        """
        Set items as attributes
        """
        setattr(self, feature, value)

    def __getitem__(self, feature):
        """
        Get the value of a feature, including 'cvarsort'
        """
        feature = feature.lower()
        if feature == 'cvarsort':
            return self.cvarsort
        elif hasattr(self, feature):
            return getattr(self, feature)
        elif feature in self.features:
            return None
        else:
            raise PydmrsKeyError("{} has no feature {}".format(type(self), feature))

    def __delitem__(self, feature):
        """
        Set the value of a feature to None
        """
        self[feature] = None

    # Initialisation

    def __init__(self, *args, **kwargs):
        """
        Initialise values of features, from positional or keyword arguments
        If a feature is not given, its value is set to None
        """
        if len(args) > len(self.features):
            raise PydmrsTypeError(
                "{} takes {} arguments, but {} were given".format(type(self).__name__,
                                                                  len(self.features),
                                                                  len(args)))
        for i, value in enumerate(args):
            setattr(self, self.features[i], value)
        for feature, value in kwargs.items():
            setattr(self, feature, value)

    # Conversion to strings and dicts

    def __str__(self):
        """
        Returns a string of the form:
        cvarsort[feature1=value1, feature2=value2, ...]
        """
        spec_feats = ', '.join('{}={}'.format(feat, self[feat]) for feat in self.features if self[feat] is not None)
        if spec_feats:
            return '{}[{}]'.format(self.cvarsort, spec_feats)
        else:
            return self.cvarsort

    def __repr__(self):
        """
        Return a string that can be evaluated
        """
        return '{}({})'.format(type(self).__name__, ', '.join(repr(self[feat]) for feat in self.features if hasattr(self, feat)))

    def as_dict(self):
        """
        Return a dict mapping from features to values, including 'cvarsort'
        """
        return dict(self.items())

    # Conversion from strings and dicts

    @classmethod
    def from_dict(cls, dictionary, **kwargs):
        """
        Instantiates a Sortinfo object from dictionary,
        normalising as necessary
        """
        normalised = cls.normalise_dict(dictionary, **kwargs)
        return cls.from_normalised_dict(normalised)

    @staticmethod
    def normalise_dict(dictionary, convert_plus_minus=True, convert_legacy_prontype=True):
        """
        Normalise a sortinfo dictionary - convert keys to lowercase and correct cvarsort if necessary
        """
        # Convert all keys to lowercase (so that we can check if certain keys exist)
        # Values are left untouched until the object is constructed (e.g. in case of None)
        dictionary = {key.lower(): value for key, value in dictionary.items()}
        # Find the cvarsort
        try:
            dictionary['cvarsort'] = dictionary['cvarsort'].lower()
        except KeyError:
            raise PydmrsValueError('Sortinfo must have cvarsort')
        # Correct cvarsort
        # 'p' is underspecified for 'h' or 'x'
        if dictionary['cvarsort'] == 'p':
            dictionary['cvarsort'] = 'x'
        # Look for features that are evidence for 'x' or 'e'
        elif dictionary['cvarsort'] not in 'ex' and len(dictionary) > 1:
            if any(key in dictionary for key in EventSortinfo.features):  # event evidence
                dictionary['cvarsort'] = 'e'
            elif any(key in dictionary for key in InstanceSortinfo.features) \
                 or 'prontype' in dictionary:  # instance evidence
                dictionary['cvarsort'] = 'x'
        
        if convert_plus_minus:
            for key, value in dictionary.items():
                if value == 'plus':
                    dictionary[key] = '+'
                if value == 'minus':
                    dictionary[key] = '-'
        
        if convert_legacy_prontype and 'prontype' in dictionary:
            pt = dictionary.pop('prontype')
            dictionary['pt'] = LegacyInstanceSortinfo.prontype_to_pt.get(pt, pt)
        
        return dictionary

    # In subclasses, _from_normalised_dict will override from_normalised_dict
    # See SortinfoMeta for details
    @staticmethod
    def from_normalised_dict(dictionary):
        """
        Instantiate a suitable type of Sortinfo from a dictionary,
        mapping from features to values (including cvarsort)
        """
        cvarsort = dictionary['cvarsort']
        # Instantiate an appropriate type of Sortinfo
        if cvarsort == 'e':
            return EventSortinfo.from_normalised_dict(dictionary)
        elif cvarsort == 'x':
            if 'prontype' in dictionary:
                return LegacyInstanceSortinfo.from_normalised_dict(dictionary)
            else:
                return InstanceSortinfo.from_normalised_dict(dictionary)
        else:
            return Sortinfo()

    @classmethod
    def _from_normalised_dict(cls, dictionary):
        """
        Instantiate from a dictionary mapping features to values
        """
        if 'cvarsort' in dictionary and dictionary['cvarsort'] != cls.cvarsort:
            raise PydmrsValueError('{} must have cvarsort {}, not {}'.format(cls.__name__,
                                                                             cls.cvarsort,
                                                                             dictionary['cvarsort']))
        return cls(**{key: value for key, value in dictionary.items() if key != 'cvarsort'})

    @classmethod
    def from_string(cls, string, **kwargs):
        """
        Instantiate from a string of the form:
        cvarsort[feature1=value1, feature2=value2, ...]
        """
        # Force lowercase
        string = string.lower()
        # Dictionary mapping from features to values
        dictionary = {'cvarsort': string[0]}
        if len(string) > 1:
            if not (string[1] == '[' and string[-1] == ']'):
                raise PydmrsValueError('Sortinfo string must include features in square brackets')
            for item in string[2:-1].split(','):
                key, value = item.split('=')
                dictionary[key.strip()] = value.strip()
        # Convert the dictionary
        return cls.from_dict(dictionary, **kwargs)

    # Comparison methods

    def __eq__(self, other):
        """
        Checks two Sortinfos for equality.
        Returns True if all specified features are the same.
        """
        return isinstance(other, Sortinfo) and self.cvarsort == other.cvarsort \
               and set(self.iter_specified()) == set(other.iter_specified())

    def __ne__(self, other):
        return not self == other

    def is_more_specific(self, other):
        """
        Checks whether this object is a more specific sortinfo than the other
        (underspecified value: '?' or 'u')
        """
        if not isinstance(other, Sortinfo):
            raise PydmrsTypeError()
        if other.cvarsort == 'i':
            return self.cvarsort != 'i'
        if self.cvarsort != other.cvarsort:
            return False
        for key, value in other.iter_specified():
            if not self.is_specified(key):
                return False
            elif value != self[key]:
                if key == 'tense' and value == 'tensed' and self[key] != 'untensed':
                    continue
                else:
                    return False
        only_this_specified = set(other.iter_specified()).difference(other.iter_specified())
        if not only_this_specified:
            return False
        return True

    def is_less_specific(self, other):
        """
        Checks whether this object is a less specific sortinfo than the other
        (underspecified value: '?' or 'u')
        """
        if not isinstance(other, Sortinfo):
            raise PydmrsTypeError()
        if self.cvarsort == 'i':
            return other.cvarsort != 'i'
        if self.cvarsort != other.cvarsort:
            return False
        for key, value in self.iter_specified():
            if not other.is_specified(key):
                return False
            elif value != other[key]:
                if key == 'tense' and value == 'tensed' and other[key] != 'untensed':
                    continue
                else:
                    return False
        only_other_specified = set(other.iter_specified()).difference(self.iter_specified())
        if not only_other_specified:
            return False
        return True class EventSortinfo(Sortinfo):
    """
    Event sortinfo
    """
    cvarsort = 'e'
    __slots__ = ('sf', 'tense', 'mood', 'perf', 'prog')
    # Sentence force, tense, mood, perfect, progressive class InstanceSortinfo(Sortinfo):
    """
    Instance sortinfo
    """
    cvarsort = 'x'
    __slots__ = ('pers', 'num', 'gend', 'ind', 'pt')
    # Person, number, gender, individuated, pronoun type
    
    def convert_to_legacy(self):
        new = LegacyInstanceSortinfo(*(self[x] for x in self.features))
        # Convert prontype value if there is a conversion (else leave the same)
        new.prontype = LegacyInstanceSortinfo.pt_to_prontype.get(new.prontype, new.prontype)
        return new class Link(namedtuple('LinkNamedTuple', ('start', 'end', 'rargname', 'post'))):
    """
    A link
    """

    __slots__ = ()  # Suppress __dict__

    def __new__(cls, start, end, rargname, post):
        """
        Create a new instance, forcing strings to be uppercase
        """
        if isinstance(rargname, str):
            rargname = rargname.upper()
        if isinstance(post, str):
            post = post.upper()
        if start == end:
            warn("Link start must not equal link end.", PydmrsWarning)
        # TODO: Pydelphin uses MOD/EQ for undirected links - make compatible.
        if rargname in ('', 'NONE', 'NULL', 'NIL'):
            rargname = None
        if post in ('', 'NONE', 'NULL', 'NIL'):
            post = None
        return super().__new__(cls, start, end, rargname, post)

    def __str__(self):
        return "({} - {}/{} -> {})".format(self.start, self.rargname, self.post, self.end)

    def __repr__(self):
        return "Link({}, {}, {}, {})".format(*(repr(x) for x in self))

    @property
    def label(self):
        return LinkLabel(self.rargname, self.post)

    @property
    def labelstring(self):
        return "{}/{}".format(self.rargname, self.post)

    def to_xml(self):
        xlink = ET.Element('link')
        xlink.set('from', str(self.start))
        xlink.set('to', str(self.end))
        xrargname = ET.SubElement(xlink, 'rargname')
        xrargname.text = self.rargname
        xpost = ET.SubElement(xlink, 'post')
        xpost.text = self.post
        return xlink

    @classmethod
    def from_xml(cls, elem):
        start = int(elem.get('from'))
        end = int(elem.get('to'))
        rargname = None
        post = None
        for sub in elem:
            if sub.tag == 'rargname':
                if sub.text != 'MOD':
                    rargname = sub.text
            elif sub.tag == 'post':
                post = sub.text
            else:
                raise PydmrsValueError(sub.tag)
        return Link(start, end, rargname, post)
 class LinkLabel(namedtuple('LinkLabelNamedTuple', ('rargname', 'post'))):
    """
    A label for a link
    """

    __slots__ = ()  # Suppress __dict__

    def __new__(cls, rargname, post):
        """
        Create new instance, forcing strings to be uppercase
        """
        if isinstance(rargname, str):
            rargname = rargname.upper()
        if isinstance(post, str):
            post = post.upper()
        return super().__new__(cls, rargname, post)

    def __str__(self):
        return "{}/{}".format(*self)

    def __repr__(self):
        return "LinkLabel({}, {})".format(*(repr(x) for x in self))

    @classmethod
    def from_string(cls, string):
        if '/' in string:
            i = string.index('/')
            rargname = string[:i]
            post = string[i+1:]
        else:
            rargname = string
            post = None
        if rargname == 'None':
            rargname = None
        elif not rargname.isupper():
            raise PydmrsValueError("Link label rargname must be upper-case.")
        if post == 'None':
            post = None
        elif not post.isupper():
            raise PydmrsValueError("Link label post must be upper-case.")
        return LinkLabel(rargname, post)
 class Node(object):
    """
    A DMRS node
    """

    def __init__(self, nodeid=None, pred=None, sortinfo=None, cfrom=None, cto=None, surface=None, base=None, carg=None):
        self.nodeid = nodeid
        self.surface = surface
        self.base = base

        if cto and cfrom and cto < cfrom:
            raise PydmrsValueError('Incorrect span: cto < cfrom.')
        self.cfrom = cfrom
        self.cto = cto

        if isinstance(pred, str):
            self.pred = Pred.from_string(pred)
        else:
            self.pred = pred

        if carg and carg[0] == '"' and carg[-1] == '"':
            carg = carg[1:-1]
        if carg and '"' in carg:
            raise PydmrsValueError('Cargs must not contain quotes.')
        self.carg = carg

        if not sortinfo:  # Allow no sortinfo
            self.sortinfo = None
        elif isinstance(sortinfo, Sortinfo):  # Allow Sortinfo instances
            self.sortinfo = sortinfo
        elif isinstance(sortinfo, dict):  # Allow initialising sortinfo from a dict
            self.sortinfo = Sortinfo.from_dict(sortinfo)
        elif isinstance(sortinfo, list):  # Allow initialising sortinfo from (key,value) pairs
            self.sortinfo = Sortinfo.from_dict({x: y for x, y in sortinfo})
        else:
            raise PydmrsTypeError("unsupported type for sortinfo")

    def __str__(self):
        string = str(self.pred)
        if self.carg:
            string += '({})'.format(self.carg)
        if self.sortinfo:
            string += ' {}'.format(self.sortinfo)
        return string

    def __eq__(self, other):
        """
        Checks two nodes for equality (predicate, carg, sortinfo)
        """
        return isinstance(other, Node) \
            and self.pred == other.pred \
            and self.carg == other.carg \
            and self.sortinfo == other.sortinfo

    def is_more_specific(self, other, hierarchy=None):
        """
        Checks whether this object is a more specific node than the other (predicate, carg, sortinfo)
        """
        if not isinstance(other, Node):
            raise PydmrsTypeError()
        result = False
        if other.pred is not None and \
            ((self.pred is None and type(other.pred) == Pred) or
             (self.pred is not None and self.pred.is_more_specific(other.pred, hierarchy=hierarchy))):
            result = True
        elif (self.pred is None) != (other.pred is None) or self.pred != other.pred:
            return False
        if self.carg != '?' and other.carg == '?':
            result = True
        elif self.carg != other.carg:
            return False
        if other.sortinfo is not None and \
            ((self.sortinfo is None and type(other.sortinfo) == Sortinfo) or
             self.sortinfo is not None and self.sortinfo.is_more_specific(other.sortinfo)):
            result = True
        elif (self.sortinfo is None) != (other.sortinfo is None) or self.sortinfo != other.sortinfo:
            return False
        return result

    def is_less_specific(self, other, hierarchy=None):
        """
        Checks whether this object is a less specific node than the other (predicate, carg, sortinfo)
        """
        if not isinstance(other, Node):
            raise PydmrsTypeError()
        result = False
        if self.pred is not None and \
            ((other.pred is None and type(self.pred) == Pred) or
             (other.pred is not None and self.pred.is_less_specific(other.pred, hierarchy=hierarchy))):
            result = True
        elif (self.pred is None) != (other.pred is None) or self.pred != other.pred:
            return False
        if self.carg == '?' and other.carg != '?':
            result = True
        elif self.carg != other.carg:
            return False
        if self.sortinfo is not None and \
            ((other.sortinfo is None and type(self.sortinfo) == Sortinfo) or
             other.sortinfo is not None and self.sortinfo.is_less_specific(other.sortinfo)):
            result = True
        elif (self.sortinfo is None) != (other.sortinfo is None) or self.sortinfo != other.sortinfo:
            return False
        return result

    @property
    def span(self):
        return self.cfrom, self.cto

    @property
    def is_gpred_node(self):
        return isinstance(self.pred, GPred)

    @property
    def is_realpred_node(self):
        return isinstance(self.pred, RealPred)

    def convert_to(self, cls):
        return cls(self.nodeid,
                   self.pred,
                   self.sortinfo,
                   self.cfrom,
                   self.cto,
                   self.surface,
                   self.base,
                   self.carg)

    def to_xml(self):
        xnode = ET.Element('node')
        xnode.set('nodeid', str(self.nodeid))
        if self.cfrom is not None and self.cto is not None:
            xnode.set('cfrom', str(self.cfrom))
            xnode.set('cto', str(self.cto))
        if self.carg:
            xnode.set('carg', '{}'.format(self.carg))
        if isinstance(self.pred, GPred):
            xpred = ET.SubElement(xnode, 'gpred')
            xpred.text = str(self.pred) + '_rel'
        elif isinstance(self.pred, RealPred):
            xpred = ET.SubElement(xnode, 'realpred')
            xpred.set('lemma', self.pred.lemma)
            xpred.set('pos', self.pred.pos)
            if self.pred.sense:
                xpred.set('sense', self.pred.sense)
        else:
            raise PydmrsTypeError("predicates must be RealPred or GPred objects")
        xsortinfo = ET.SubElement(xnode, 'sortinfo')
        if self.sortinfo:
            for key in self.sortinfo:
                value = self.sortinfo[key]
                if value:
                    xsortinfo.set(key, value)
        return xnode

    @classmethod
    def from_xml(cls, elem, convert_legacy_prontype=True):
        nodeid = int(elem.get('nodeid')) if 'nodeid' in elem.attrib else None
        cfrom = int(elem.get('cfrom')) if 'cfrom' in elem.attrib else None
        cto = int(elem.get('cto')) if 'cto' in elem.attrib else None
        surface = elem.get('surface')
        base = elem.get('base')
        carg = elem.get('carg')

        pred = None  # Default value
        sortinfo = None  # Default value
        for sub in elem:
            if sub.tag == 'realpred':
                try:
                    pred = RealPred(sub.get('lemma').lower(), sub.get('pos'), sub.get('sense'))
                except PydmrsValueError:
                    # If the whole pred name is under 'lemma', rather than split between 'lemma', 'pos', 'sense'
                    pred = RealPred.from_string(sub.get('lemma'))
                    warn("RealPred given as string rather than lemma, pos, sense", PydmrsWarning)
            elif sub.tag == 'gpred':
                try:
                    pred = GPred.from_string(sub.text)
                except PydmrsValueError:
                    # If the string is actually for a RealPred, not a GPred
                    pred = RealPred.from_string(sub.text)
                    warn("RealPred string found in a <gpred> tag", PydmrsWarning)
            elif sub.tag == 'sortinfo':
                if sub.attrib:  # If sub.attrib is empty, leave sortinfo as None
                    sortinfo = Sortinfo.from_dict(sub.attrib,
                                                  convert_legacy_prontype=convert_legacy_prontype)
            else:
                raise PydmrsValueError(sub.tag)
        return cls(nodeid=nodeid, pred=pred, carg=carg, sortinfo=sortinfo, cfrom=cfrom, cto=cto,
                   surface=surface,
                   base=base)
 def span_pred_key(node):
    """
    For use as a node_key in SortDictDmrs.
    This sorts nodes by: cfrom (ascending), cto (descending), predstring (ascending)
    """
    return (node.cfrom, -node.cto, str(node.pred))
 def abstractSortDictDmrs(node_key=None, link_key=None):
    """
    For constructing SortDictDmrs objects with the same node_key and link_key functions.
    :param node_key: function to get keys for nodes
        (default: nodeid)
    :param link_key: function to get keys for links
        (default: start key, end key, rargname, post)
    :return: a factory function that constructs SortDictDmrs instances with these keys
    """
    def wrapper(*args, **kwargs):
        """
        A factory function that constructs SortDictDmrs instances with specific keys.
        """
        return SortDictDmrs(*args, node_key=node_key, link_key=link_key, **kwargs)
    wrapper.Node = SortDictDmrs.Node
    return wrapper
 def the():
def the_cat():
def the_mouse():
def dog_cat():
def the_dog_chases_the_cat():
def the_cat_chases_the_dog():
def the_dog_chases_the_mouse():
def the_dog_chases_the_cat_and_the_mouse():
def the_dog_chases_the_cat_and_the_cat_chases_the_mouse():
def predsort():
def noun():
import unittest
import warnings
from pydmrs._exceptions import PydmrsTypeError, PydmrsValueError
from pydmrs.components import Pred, GPred, Sortinfo, EventSortinfo, InstanceSortinfo
from pydmrs.core import (
    Link, LinkLabel,
    Node, span_pred_key, abstractSortDictDmrs)
from examples import examples_dmrs
        from copy import copy, deepcopy
        from copy import copy, deepcopy

        link = Link(0, 1, 'RSTR', 'H')
        label = LinkLabel('RSTR', 'H')
        self.assertIsInstance(link.label, LinkLabel)
        self.assertEqual(link.label, label)

    def test_Link_labelstring(self):
        """
        The labelstring of a link should be its label's string 
        """
        link = Link(0, 1, 'RSTR', 'H')
        labelstring = 'RSTR/H'
        self.assertEqual(link.labelstring, labelstring)

    def test_Link_copy(self):
        """
        copy.copy should return an equal Link
        copy.deepcopy should also return an equal Link
        """
        link = Link(0, 1, 'RSTR', 'H')
        link_copy = copy(link)
        link_deep = deepcopy(link)
        self.assertEqual(link, link_copy)
        self.assertEqual(link, link_deep)
        self.assertIsNot(link, link_copy)
        self.assertIsNot(link, link_deep)
        # Note that it doesn't make sense to check
        # if link.end is not link_deep.end,
        # because identical strings and ints are considered to be the same

    def test_LinkLabel_new(self):
        """
        LinkLabels should have exactly two slots (rargname, post).
        The constructor should take either positional or keyword arguments.
        The slots should be accessible by attribute names.
        """
        # Check two arguments
        self.assert_rstr_h(LinkLabel('RSTR', 'H'))
        self.assert_rstr_h(LinkLabel(rargname='RSTR', post='H'))

        # Check wrong numbers of arguments
        with self.assertRaises(TypeError):
            LinkLabel(0, 1, 2)
        with self.assertRaises(TypeError):
            LinkLabel(0, 1, 2, 3, 4)

    # Helper function for test_LinkLabel_new
    def assert_rstr_h(self, linklabel):
        self.assertEqual(linklabel.rargname, 'RSTR')
        self.assertEqual(linklabel.post, 'H')

    def test_LinkLabel_str(self):
        """
        The 'informal' string representation of a LinkLabel
        should have a slash between the rargname and post
        """
        label = LinkLabel('RSTR', 'H')
        self.assertEqual(str(label), "RSTR/H")

    def test_LinkLabel_repr(self):
        """
        The 'official' string representation of a LinkLabel
        should evaluate to an equivalent LinkLabel
        """
        label = LinkLabel('RSTR', 'H')
        self.assertEqual(label, eval(repr(label)))

    def test_LinkLabel_copy(self):
        """
        copy.copy should return an equal LinkLabel
        copy.deepcopy should also return an equal LinkLabel
        """
        label = LinkLabel('RSTR', 'H')
        label_copy = copy(label)
        label_deep = deepcopy(label)
        self.assertEqual(label, label_copy)
        self.assertEqual(label, label_deep)
        self.assertIsNot(label, label_copy)
        self.assertIsNot(label, label_deep)
        # Note that it doesn't make sense to check
        # if label.post is not label_deep.post,
        # because identical strings are considered to be the same


class TestNode(unittest.TestCase):
    """
    Test methods for Node class.
    """

    def test_Node_init(self):
        node = Node(nodeid=13, pred='the_q', surface='cat', base='x', cfrom=23, cto=27,
                    carg='Kim', )
        self.assertEqual(node.nodeid, 13)
        self.assertEqual(node.surface, 'cat')
        self.assertEqual(node.base, 'x')

        self.assertEqual(node.cfrom, 23)
        self.assertEqual(node.cto, 27)
        # Incorrect span
        with self.assertRaises(PydmrsValueError):
            Node(cfrom=22, cto=7)

        self.assertEqual(node.carg, 'Kim')
        # Fix carg with  "".
        self.assertEqual(Node(carg='"Kim"').carg, 'Kim')
        # Unaccounted " in carg
        with self.assertRaises(PydmrsValueError):
            Node(carg='"Kim')

        # String pred.
        self.assertEqual(node.pred, GPred('the_q'))
        # Other pred
        self.assertEqual(Node(pred=GPred('the_q')).pred, GPred('the_q'))

        # Allow None for sortinfo.
        self.assertEqual(Node().sortinfo, None)
        # Dict sortinfo
        self.assertEqual(Node(sortinfo={'cvarsort': 'i', 'pers': '3'}).sortinfo,
                         InstanceSortinfo(pers='3'))
        # Sortinfo sortinfo
        self.assertEqual(Node(sortinfo=InstanceSortinfo(pers='3')).sortinfo,

                         InstanceSortinfo(pers='3'))
