#
# LSST Data Management System
#
# This product includes software developed by the
# LSST Project (http://www.lsst.org/).
#
# See COPYRIGHT file at the top of the source tree.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the LSST License Statement and
# the GNU General Public License along with this program.  If not,
# see <https://www.lsstcorp.org/LegalNotices/>.
#
"""Tools for building and parsing fully-qualified names of metrics and
specifications.
"""
from __future__ import print_function
__all__ = ['Name']
[docs]class Name(object):
    """Semantic name of a package, `~lsst.verify.Metric` or
    `~lsst.verify.Specification` in the `lsst.verify` framework.
    ``Name`` instances are immutable and can be used as keys in mappings.
    Parameters
    ----------
    package : `str` or `Name`
       Name of the package, either as a string (``'validate_drp'``,
       for example) or as a `~lsst.verify.Name` object
       (``Name(package='validate_drp')`` for example).
       The ``package`` field can also be fully specified:
           >>> Name(package='validate_drp.PA1.design_gri')
           Name('validate_drp', 'PA1', 'design_gri')
       Or the ``package`` field can be used as the sole positional argument:
           >>> Name('validate_drp.PA1.design_gri')
           Name('validate_drp', 'PA1', 'design_gri')
    metric : `str` or `Name`
       Name of the metric. The name can be relative (``'PA'``) or
       fully-specified (``'validate_drp.PA1'``).
    spec : `str` or `Name`
       Name of the specification. The name can be bare (``'design_gri'``),
       metric-relative (``'PA1.design_gri'``) or fully-specified
       (``'validate_drp.PA1.design_gri'``)
    Raises
    ------
    TypeError
       Raised when arguments cannot be parsed or conflict (for example, if two
       different package names are specified through two different fields).
    Notes
    -----
    Names in the Verification Framework are formatted as::
        package.metric.specification
    A **fully-qualified name** is one that has all components included. For
    example, a fully specified metric name is::
        validate_drp.PA1
    This means: "the ``PA1`` metric in the ``validate_drp`` package.
    An example of a fully-specificed *specification* name::
        validate_drp.PA1.design
    This means: "the ``design`` specification of the ``PA1`` metric in the
    ``validate_drp`` package.
    A **relative name** is one that's missing a component. For example::
        PA1.design
    Asserting this is a relative specification name, the package is not known.
    Examples
    --------
    **Creation**
    There are many patterns for creating Name instances. Different patterns
    are useful in different circumstances.
    You can create a metric name from its components:
    >>> Name(package='validate_drp', metric='PA1')
    Name('validate_drp', 'PA1')
    Or a specification name from components:
    >>> Name(package='validate_drp', metric='PA1', spec='design')
    Name('validate_drp', 'PA1', 'design')
    You can equivalently create ``Name``\ s from fully-qualified strings:
    >>> Name('validate_drp.PA1')
    Name('validate_drp', 'PA1')
    >>> Name('validate_drp.PA1.design')
    Name('validate_drp', 'PA1', 'design')
    You can also use an existing name to specify some components of the name:
    >>> metric_name = Name('validate_drp.PA1')
    >>> Name(metric=metric_name, spec='design')
    Name('validate_drp', 'PA1', 'design')
    A `TypeError` is raised if any components of the input names conflict.
    Fully-specified metric names can be mixed with a ``spec`` component:
    >>> Name(metric='validate_drp.PA1', spec='design')
    Name('validate_drp', 'PA1', 'design')
    **String representation**
    Converting a ``Name`` into a `str` gives you the canonical string
    representation, as fully-specified as possible:
    >>> str(Name('validate_drp', 'PA1', 'design'))
    'validate_drp.PA1.design'
    Alternatively, obtain the fully-qualified metric name from the
    `Name.fqn` property:
    >>> name = Name('validate_drp', 'PA1', 'design')
    >>> name.fqn
    'validate_drp.PA1.design'
    The relative name of a specification omits the package component:
    >>> name = Name('validate_drp.PA1.design')
    >>> name.relative_name
    'PA1.design'
    """
    def __init__(self, package=None, metric=None, spec=None):
        self._package = None
        self._metric = None
        self._spec = None
        if package is not None:
            if isinstance(package, Name):
                self._package = package.package
                self._metric = package.metric
                self._spec = package.spec
            else:
                # Assume a string type
                try:
                    self._package, self._metric, self._spec = \
                        
Name._parse_fqn_string(package)
                except ValueError as e:
                    # Want to raise TypeError in __init__
                    raise TypeError(str(e))
        if metric is not None:
            if isinstance(metric, Name):
                if metric.has_metric is False:
                    raise TypeError(
                        'metric={metric!r} argument does not include metric '
                        'information.'.format(metric=metric))
                _package = metric.package
                _metric = metric.metric
                _spec = metric.spec
            else:
                try:
                    _package, _metric = \
                        
Name._parse_metric_name_string(metric)
                    _spec = None
                except ValueError as e:
                    # Want to raise TypeError in __init__
                    raise TypeError(str(e))
            # Ensure none of the new information is inconsistent
            self._init_new_package_info(_package)
            self._init_new_metric_info(_metric)
            self._init_new_spec_info(_spec)
        if spec is not None:
            if isinstance(spec, Name):
                if spec.has_spec is False:
                    raise TypeError(
                        'spec={spec!r} argument does not include '
                        'specification information'.format(spec=spec))
                _package = spec.package
                _metric = spec.metric
                _spec = spec.spec
            else:
                try:
                    _package, _metric, _spec = \
                        
Name._parse_spec_name_string(spec)
                except ValueError as e:
                    # want to raise TypeError in __init__
                    raise TypeError(str(e))
            # Ensure none of the new information is inconsistent
            self._init_new_package_info(_package)
            self._init_new_metric_info(_metric)
            self._init_new_spec_info(_spec)
        # Ensure the name doesn't have a metric gap
        if self._package is not None \
                
and self._spec is not None \
                
and self._metric is None:
            raise TypeError("Missing 'metric' given package={package!r} "
                            "spec={spec!r}".format(package=package,
                                                   spec=spec))
    def _init_new_package_info(self, package):
        """Check and add new package information (for __init__)."""
        if package is not None:
            if self._package is None or package == self._package:
                # There's new or consistent package info
                self._package = package
            else:
                message = 'You provided a conflicting package={package!r}.'
                raise TypeError(message.format(package=package))
    def _init_new_metric_info(self, metric):
        """Check and add new metric information (for __init__)."""
        if metric is not None:
            if self._metric is None or metric == self._metric:
                # There's new or consistent metric info
                self._metric = metric
            else:
                message = 'You provided a conflicting metric={metric!r}.'
                raise TypeError(message.format(metric=metric))
    def _init_new_spec_info(self, spec):
        """Check and add new spec information (for __init__)."""
        if spec is not None:
            if self._spec is None or spec == self._spec:
                # There's new or consistent spec info
                self._spec = spec
            else:
                message = 'You provided a conflicting spec={spec!r}.'
                raise TypeError(message.format(spec=spec))
    @staticmethod
    def _parse_fqn_string(fqn):
        """Parse a fully-qualified name.
        """
        parts = fqn.split('.')
        if len(parts) == 1:
            # Must be a package name alone
            return parts[0], None, None
        if len(parts) == 2:
            # Must be a fully-qualified metric name
            return parts[0], parts[1], None
        elif len(parts) == 3:
            # Must be a fully-qualified specification name
            return parts
        else:
            # Don't know what this string is
            raise ValueError('Cannot parse fully qualified name: '
                             '{0!r}'.format(fqn))
    @staticmethod
    def _parse_metric_name_string(name):
        """Parse a metric name."""
        parts = name.split('.')
        if len(parts) == 2:
            # Must be a fully-qualified metric name
            return parts[0], parts[1]
        elif len(parts) == 1:
            # A bare metric name
            return None, parts[0]
        else:
            # Don't know what this string is
            raise ValueError('Cannot parse metric name: '
                             '{0!r}'.format(name))
    @staticmethod
    def _parse_spec_name_string(name):
        """Parse a specification name."""
        parts = name.split('.')
        if len(parts) == 1:
            # Bare specification name
            return None, None, parts[0]
        elif len(parts) == 2:
            # metric-relative specification name
            return None, parts[0], parts[1]
        elif len(parts) == 3:
            # fully-qualified specification name
            return parts
        else:
            # Don't know what this string is
            raise ValueError('Cannot parse specification name: '
                             '{0!r}'.format(name))
    @property
    def package(self):
        """Package name (`str`).
        >>> name = Name('validate_drp.PA1.design')
        >>> name.package
        'validate_drp'
        """
        return self._package
    @property
    def metric(self):
        """Metric name (`str`).
        >>> name = Name('validate_drp.PA1.design')
        >>> name.metric
        'PA1'
        """
        return self._metric
    @property
    def spec(self):
        """Specification name (`str`).
        >>> name = Name('validate_drp.PA1.design')
        >>> name.spec
        'design'
        """
        return self._spec
    def __eq__(self, other):
        """Test equality of two specifications.
        Examples
        --------
        >>> name1 = Name('validate_drp.PA1.design')
        >>> name2 = Name('validate_drp', 'PA1', 'design')
        >>> name1 == name2
        True
        """
        return (self.package == other.package) and \
            
(self.metric == other.metric) and \
            
(self.spec == other.spec)
    def __ne__(self, other):
        return not self.__eq__(other)
    def __lt__(self, other):
        """Test self < other to support name ordering."""
        # test package component first
        if self.package < other.package:
            return True
        elif self.package > other.package:
            return False
        # test metric component second if packages equal
        if self.metric < other.metric:
            return True
        elif self.metric > other.metric:
            return False
        # test spec component lastly if everything else equal
        if self.spec < other.spec:
            return True
        elif self.spec > other.spec:
            return False
        # They're equal
        return False
    def __gt__(self, other):
        """Test self > other to support name ordering."""
        # test package component first
        if self.package > other.package:
            return True
        elif self.package < other.package:
            return False
        # test metric component second if packages equal
        if self.metric > other.metric:
            return True
        elif self.metric < other.metric:
            return False
        # test spec component lastly if everything else equal
        if self.spec > other.spec:
            return True
        elif self.spec < other.spec:
            return False
        # They're equal
        return False
    def __le__(self, other):
        """Test self <= other to support name ordering."""
        if self.__eq__(other):
            return True
        else:
            return self.__lt__(other)
    def __ge__(self, other):
        """Test self >= other to support name ordering."""
        if self.__eq__(other):
            return True
        else:
            return self.__gt__(other)
    def __hash__(self):
        return hash((self.package, self.metric, self.spec))
    def __contains__(self, name):
        """Test if another Name is contained by this Name.
        A specification can be in a metric and a package. A metric can be in
        a package.
        Examples
        --------
        >>> spec_name = Name('validate_drp.PA1.design')
        >>> metric_name = Name('validate_drp.PA1')
        >>> package_name = Name('validate_drp')
        >>> spec_name in metric_name
        True
        >>> package_name in metric_name
        False
        """
        contains = True  # tests will disprove membership
        if self.is_package:
            if name.is_package:
                contains = False
            else:
                contains = self.package == name.package
        elif self.is_metric:
            if name.is_metric:
                contains = False
            else:
                if self.has_package or name.has_package:
                    contains = contains and (self.package == name.package)
                contains = contains and (self.metric == name.metric)
        else:
            # Must be a specification, which cannot 'contain' anything
            contains = False
        return contains
    @property
    def has_package(self):
        """`True` if this object contains a package name (`bool`).
        >>> Name('validate_drp.PA1').has_package
        True
        >>> Name(spec='design').has_package
        False
        """
        if self.package is not None:
            return True
        else:
            return False
    @property
    def has_spec(self):
        """`True` if this object contains a specification name, either
        relative or fully-qualified (`bool`).
        >>> Name(spec='design').has_spec
        True
        >>> Name('validate_drp.PA1').has_spec
        False
        """
        if self.spec is not None:
            return True
        else:
            return False
    @property
    def has_metric(self):
        """`True` if this object contains a metric name, either
        relative or fully-qualified (`bool`).
        >>> Name('validate_drp.PA1').has_metric
        True
        >>> Name(spec='design').has_metric
        False
        """
        if self.metric is not None:
            return True
        else:
            return False
    @property
    def has_relative(self):
        """`True` if a relative specification name can be formed from this
        object, i.e., `metric` and `spec` attributes are set (`bool`).
        """
        if self.is_spec and self.has_metric:
            return True
        else:
            return False
    @property
    def is_package(self):
        """`True` if this object is a package name (`bool`).
        >>> Name('validate_drp').is_package
        True
        >>> Name('validate_drp.PA1').is_package
        False
        """
        if self.has_package and \
                
self.is_metric is False and \
                
self.is_spec is False:
            return True
        else:
            return False
    @property
    def is_metric(self):
        """`True` if this object is a metric name, either relative or
        fully-qualified (`bool`).
        >>> Name('validate_drp.PA1').is_metric
        True
        >>> Name('validate_drp.PA1.design').is_metric
        False
        """
        if self.has_metric is True and self.has_spec is False:
            return True
        else:
            return False
    @property
    def is_spec(self):
        """`True` if this object is a specification name, either relative or
        fully-qualified (`bool`).
        >>> Name('validate_drp.PA1').is_spec
        False
        >>> Name('validate_drp.PA1.design').is_spec
        True
        """
        if self.has_spec is True:
            return True
        else:
            return False
    @property
    def is_fq(self):
        """`True` if this object is a fully-qualified name of either a
        package, metric or specification (`bool`).
        Examples
        --------
        A fully-qualified package name:
        >>> Name('validate_drp').is_fq
        True
        A fully-qualified metric name:
        >>> Name('validate_drp.PA1').is_fq
        True
        A fully-qualified specification name:
        >>> Name('validate_drp.PA1.design_gri').is_fq
        True
        """
        if self.is_package:
            # package names are by definition fully qualified
            return True
        elif self.is_metric and self.has_package:
            # fully-qualified metric
            return True
        elif self.is_spec and self.has_package and self.has_metric:
            # fully-qualified specification
            return True
        else:
            return False
    @property
    def is_relative(self):
        """`True` if this object is a specification name that's not
        fully-qualified, but is relative to a metric name (`bool`).
        relative to a base name. (`bool`).
        Package and metric names are never relative.
        A relative specification name:
        >>> Name(spec='PA1.design_gri').is_relative
        True
        But not:
        >>> Name('validate_drp.PA1.design_gri').is_relative
        False
        """
        if self.is_spec and \
                
self.has_metric is True and \
                
self.has_package is False:
            return True
        else:
            return False
    def __repr__(self):
        if self.is_package:
            return 'Name({self.package!r})'.format(self=self)
        elif self.is_metric and not self.is_fq:
            return 'Name(metric={self.metric!r})'.format(self=self)
        elif self.is_metric and self.is_fq:
            return 'Name({self.package!r}, {self.metric!r})'.format(
                self=self)
        elif self.is_spec and not self.is_fq and not self.is_relative:
            return 'Name(spec={self.spec!r})'.format(
                self=self)
        elif self.is_spec and not self.is_fq and self.is_relative:
            return 'Name(metric={self.metric!r}, spec={self.spec!r})'.format(
                self=self)
        else:
            # Should be a fully-qualified specification
            template = 'Name({self.package!r}, {self.metric!r}, {self.spec!r})'
            return template.format(self=self)
    def __str__(self):
        """Canonical string representation of a Name (`str`).
        Examples:
        >>> str(Name(package='validate_drp'))
        'validate_drp'
        >>> str(Name(package='validate_drp', metric='PA1'))
        'validate_drp.PA1'
        >>> str(Name(package='validate_drp', metric='PA1', spec='design'))
        'validate_drp.PA1.design'
        >>> str(Name(metric='PA1', spec='design'))
        'PA1.design'
        """
        if self.is_package:
            return self.package
        elif self.is_metric and not self.is_fq:
            return self.metric
        elif self.is_metric and self.is_fq:
            return '{self.package}.{self.metric}'.format(self=self)
        elif self.is_spec and not self.is_fq and not self.is_relative:
            return self.spec
        elif self.is_spec and not self.is_fq and self.is_relative:
            return '{self.metric}.{self.spec}'.format(self=self)
        else:
            # Should be a fully-qualified specification
            return '{self.package}.{self.metric}.{self.spec}'.format(
                self=self)
    @property
    def fqn(self):
        """The fully-qualified name (`str`).
        Raises
        ------
        AttributeError
           If the name is not a fully-qualified name (check `is_fq`)
        Examples
        --------
        >>> Name('validate_drp', 'PA1').fqn
        'validate_drp.PA1'
        >>> Name('validate_drp', 'PA1', 'design').fqn
        'validate_drp.PA1.design'
        """
        if self.is_fq:
            return str(self)
        else:
            message = '{self!r} is not a fully-qualified name'
            raise AttributeError(message.format(self=self))
    @property
    def relative_name(self):
        """The relative specification name (`str`).
        Raises
        ------
        AttributeError
           If the object does not represent a specification, or if a relative
           name cannot be formed because the `metric` is None.
        Examples
        --------
        >>> Name('validate_drp.PA1.design').relative_name
        'PA1.design'
        """
        if self.has_relative:
            return '{self.metric}.{self.spec}'.format(self=self)
        else:
            message = '{self!r} is not a relative specification name'
            raise AttributeError(message.format(self=self))