#
# 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))