Coverage for python/lsst/verify/naming.py: 21%
244 statements
« prev ^ index » next coverage.py v7.4.3, created at 2024-03-14 17:24 +0000
« prev ^ index » next coverage.py v7.4.3, created at 2024-03-14 17:24 +0000
1# This file is part of verify.
2#
3# Developed for the LSST Data Management System.
4# This product includes software developed by the LSST Project
5# (https://www.lsst.org).
6# See the COPYRIGHT file at the top-level directory of this distribution
7# for details of code ownership.
8#
9# This program is free software: you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation, either version 3 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with this program. If not, see <https://www.gnu.org/licenses/>.
21"""Tools for building and parsing fully-qualified names of metrics and
22specifications.
23"""
25__all__ = ['Name']
28class Name(object):
29 r"""Semantic name of a package, `~lsst.verify.Metric` or
30 `~lsst.verify.Specification` in the `lsst.verify` framework.
32 ``Name`` instances are immutable and can be used as keys in mappings.
34 Parameters
35 ----------
36 package : `str` or `Name`
37 Name of the package, either as a string (``'validate_drp'``,
38 for example) or as a `~lsst.verify.Name` object
39 (``Name(package='validate_drp')`` for example).
41 The ``package`` field can also be fully specified:
43 >>> Name(package='validate_drp.PA1.design_gri')
44 Name('validate_drp', 'PA1', 'design_gri')
46 Or the ``package`` field can be used as the sole positional argument:
48 >>> Name('validate_drp.PA1.design_gri')
49 Name('validate_drp', 'PA1', 'design_gri')
51 metric : `str` or `Name`
52 Name of the metric. The name can be relative (``'PA'``) or
53 fully-specified (``'validate_drp.PA1'``).
55 spec : `str` or `Name`
56 Name of the specification. The name can be bare (``'design_gri'``),
57 metric-relative (``'PA1.design_gri'``) or fully-specified
58 (``'validate_drp.PA1.design_gri'``)
60 Raises
61 ------
62 TypeError
63 Raised when arguments cannot be parsed or conflict (for example, if two
64 different package names are specified through two different fields).
66 Notes
67 -----
68 Names in the Verification Framework are formatted as::
70 package.metric.specification
72 A **fully-qualified name** is one that has all components included. For
73 example, a fully specified metric name is::
75 validate_drp.PA1
77 This means: "the ``PA1`` metric in the ``validate_drp`` package.
79 An example of a fully-specificed *specification* name::
81 validate_drp.PA1.design
83 This means: "the ``design`` specification of the ``PA1`` metric in the
84 ``validate_drp`` package.
86 A **relative name** is one that's missing a component. For example::
88 PA1.design
90 Asserting this is a relative specification name, the package is not known.
92 Examples
93 --------
95 **Creation**
97 There are many patterns for creating Name instances. Different patterns
98 are useful in different circumstances.
100 You can create a metric name from its components:
102 >>> Name(package='validate_drp', metric='PA1')
103 Name('validate_drp', 'PA1')
105 Or a specification name from components:
107 >>> Name(package='validate_drp', metric='PA1', spec='design')
108 Name('validate_drp', 'PA1', 'design')
110 You can equivalently create ``Name``\ s from fully-qualified strings:
112 >>> Name('validate_drp.PA1')
113 Name('validate_drp', 'PA1')
115 >>> Name('validate_drp.PA1.design')
116 Name('validate_drp', 'PA1', 'design')
118 You can also use an existing name to specify some components of the name:
120 >>> metric_name = Name('validate_drp.PA1')
121 >>> Name(metric=metric_name, spec='design')
122 Name('validate_drp', 'PA1', 'design')
124 A `TypeError` is raised if any components of the input names conflict.
126 Fully-specified metric names can be mixed with a ``spec`` component:
128 >>> Name(metric='validate_drp.PA1', spec='design')
129 Name('validate_drp', 'PA1', 'design')
131 **String representation**
133 Converting a ``Name`` into a `str` gives you the canonical string
134 representation, as fully-specified as possible:
136 >>> str(Name('validate_drp', 'PA1', 'design'))
137 'validate_drp.PA1.design'
139 Alternatively, obtain the fully-qualified metric name from the
140 `Name.fqn` property:
142 >>> name = Name('validate_drp', 'PA1', 'design')
143 >>> name.fqn
144 'validate_drp.PA1.design'
146 The relative name of a specification omits the package component:
148 >>> name = Name('validate_drp.PA1.design')
149 >>> name.relative_name
150 'PA1.design'
151 """
153 def __init__(self, package=None, metric=None, spec=None):
154 self._package = None
155 self._metric = None
156 self._spec = None
158 if package is not None:
159 if isinstance(package, Name):
160 self._package = package.package
161 self._metric = package.metric
162 self._spec = package.spec
163 else:
164 # Assume a string type
165 try:
166 self._package, self._metric, self._spec = \
167 Name._parse_fqn_string(package)
168 except ValueError as e:
169 # Want to raise TypeError in __init__
170 raise TypeError(str(e))
172 if metric is not None:
173 if isinstance(metric, Name):
174 if metric.has_metric is False:
175 raise TypeError(
176 'metric={metric!r} argument does not include metric '
177 'information.'.format(metric=metric))
178 _package = metric.package
179 _metric = metric.metric
180 _spec = metric.spec
181 else:
182 try:
183 _package, _metric = \
184 Name._parse_metric_name_string(metric)
185 _spec = None
186 except ValueError as e:
187 # Want to raise TypeError in __init__
188 raise TypeError(str(e))
190 # Ensure none of the new information is inconsistent
191 self._init_new_package_info(_package)
192 self._init_new_metric_info(_metric)
193 self._init_new_spec_info(_spec)
195 if spec is not None:
196 if isinstance(spec, Name):
197 if spec.has_spec is False:
198 raise TypeError(
199 'spec={spec!r} argument does not include '
200 'specification information'.format(spec=spec))
201 _package = spec.package
202 _metric = spec.metric
203 _spec = spec.spec
204 else:
205 try:
206 _package, _metric, _spec = \
207 Name._parse_spec_name_string(spec)
208 except ValueError as e:
209 # want to raise TypeError in __init__
210 raise TypeError(str(e))
212 # Ensure none of the new information is inconsistent
213 self._init_new_package_info(_package)
214 self._init_new_metric_info(_metric)
215 self._init_new_spec_info(_spec)
217 # Ensure the name doesn't have a metric gap
218 if self._package is not None \
219 and self._spec is not None \
220 and self._metric is None:
221 raise TypeError("Missing 'metric' given package={package!r} "
222 "spec={spec!r}".format(package=package,
223 spec=spec))
225 def _init_new_package_info(self, package):
226 """Check and add new package information (for __init__)."""
227 if package is not None:
228 if self._package is None or package == self._package:
229 # There's new or consistent package info
230 self._package = package
231 else:
232 message = 'You provided a conflicting package={package!r}.'
233 raise TypeError(message.format(package=package))
235 def _init_new_metric_info(self, metric):
236 """Check and add new metric information (for __init__)."""
237 if metric is not None:
238 if self._metric is None or metric == self._metric:
239 # There's new or consistent metric info
240 self._metric = metric
241 else:
242 message = 'You provided a conflicting metric={metric!r}.'
243 raise TypeError(message.format(metric=metric))
245 def _init_new_spec_info(self, spec):
246 """Check and add new spec information (for __init__)."""
247 if spec is not None:
248 if self._spec is None or spec == self._spec:
249 # There's new or consistent spec info
250 self._spec = spec
251 else:
252 message = 'You provided a conflicting spec={spec!r}.'
253 raise TypeError(message.format(spec=spec))
255 @staticmethod
256 def _parse_fqn_string(fqn):
257 """Parse a fully-qualified name.
258 """
259 parts = fqn.split('.')
260 if len(parts) == 1:
261 # Must be a package name alone
262 return parts[0], None, None
263 if len(parts) == 2:
264 # Must be a fully-qualified metric name
265 return parts[0], parts[1], None
266 elif len(parts) == 3:
267 # Must be a fully-qualified specification name
268 return parts
269 else:
270 # Don't know what this string is
271 raise ValueError('Cannot parse fully qualified name: '
272 '{0!r}'.format(fqn))
274 @staticmethod
275 def _parse_metric_name_string(name):
276 """Parse a metric name."""
277 parts = name.split('.')
278 if len(parts) == 2:
279 # Must be a fully-qualified metric name
280 return parts[0], parts[1]
281 elif len(parts) == 1:
282 # A bare metric name
283 return None, parts[0]
284 else:
285 # Don't know what this string is
286 raise ValueError('Cannot parse metric name: '
287 '{0!r}'.format(name))
289 @staticmethod
290 def _parse_spec_name_string(name):
291 """Parse a specification name."""
292 parts = name.split('.')
293 if len(parts) == 1:
294 # Bare specification name
295 return None, None, parts[0]
296 elif len(parts) == 2:
297 # metric-relative specification name
298 return None, parts[0], parts[1]
299 elif len(parts) == 3:
300 # fully-qualified specification name
301 return parts
302 else:
303 # Don't know what this string is
304 raise ValueError('Cannot parse specification name: '
305 '{0!r}'.format(name))
307 @property
308 def package(self):
309 """Package name (`str`).
311 >>> name = Name('validate_drp.PA1.design')
312 >>> name.package
313 'validate_drp'
314 """
315 return self._package
317 @property
318 def metric(self):
319 """Metric name (`str`).
321 >>> name = Name('validate_drp.PA1.design')
322 >>> name.metric
323 'PA1'
324 """
325 return self._metric
327 @property
328 def spec(self):
329 """Specification name (`str`).
331 >>> name = Name('validate_drp.PA1.design')
332 >>> name.spec
333 'design'
334 """
335 return self._spec
337 def __eq__(self, other):
338 """Test equality of two specifications.
340 Examples
341 --------
342 >>> name1 = Name('validate_drp.PA1.design')
343 >>> name2 = Name('validate_drp', 'PA1', 'design')
344 >>> name1 == name2
345 True
346 """
347 return (self.package == other.package) and \
348 (self.metric == other.metric) and \
349 (self.spec == other.spec)
351 def __ne__(self, other):
352 return not self.__eq__(other)
354 def __lt__(self, other):
355 """Test self < other to support name ordering."""
356 # test package component first
357 if self.package < other.package:
358 return True
359 elif self.package > other.package:
360 return False
362 # test metric component second if packages equal
363 if self.metric < other.metric:
364 return True
365 elif self.metric > other.metric:
366 return False
368 # test spec component lastly if everything else equal
369 if self.spec < other.spec:
370 return True
371 elif self.spec > other.spec:
372 return False
374 # They're equal
375 return False
377 def __gt__(self, other):
378 """Test self > other to support name ordering."""
379 # test package component first
380 if self.package > other.package:
381 return True
382 elif self.package < other.package:
383 return False
385 # test metric component second if packages equal
386 if self.metric > other.metric:
387 return True
388 elif self.metric < other.metric:
389 return False
391 # test spec component lastly if everything else equal
392 if self.spec > other.spec:
393 return True
394 elif self.spec < other.spec:
395 return False
397 # They're equal
398 return False
400 def __le__(self, other):
401 """Test self <= other to support name ordering."""
402 if self.__eq__(other):
403 return True
404 else:
405 return self.__lt__(other)
407 def __ge__(self, other):
408 """Test self >= other to support name ordering."""
409 if self.__eq__(other):
410 return True
411 else:
412 return self.__gt__(other)
414 def __hash__(self):
415 return hash((self.package, self.metric, self.spec))
417 def __contains__(self, name):
418 """Test if another Name is contained by this Name.
420 A specification can be in a metric and a package. A metric can be in
421 a package.
423 Examples
424 --------
425 >>> spec_name = Name('validate_drp.PA1.design')
426 >>> metric_name = Name('validate_drp.PA1')
427 >>> package_name = Name('validate_drp')
428 >>> spec_name in metric_name
429 True
430 >>> package_name in metric_name
431 False
432 """
433 contains = True # tests will disprove membership
435 if self.is_package:
436 if name.is_package:
437 contains = False
438 else:
439 contains = self.package == name.package
441 elif self.is_metric:
442 if name.is_metric:
443 contains = False
444 else:
445 if self.has_package or name.has_package:
446 contains = contains and (self.package == name.package)
448 contains = contains and (self.metric == name.metric)
450 else:
451 # Must be a specification, which cannot 'contain' anything
452 contains = False
454 return contains
456 @property
457 def has_package(self):
458 """`True` if this object contains a package name (`bool`).
460 >>> Name('validate_drp.PA1').has_package
461 True
462 >>> Name(spec='design').has_package
463 False
464 """
465 if self.package is not None:
466 return True
467 else:
468 return False
470 @property
471 def has_spec(self):
472 """`True` if this object contains a specification name, either
473 relative or fully-qualified (`bool`).
475 >>> Name(spec='design').has_spec
476 True
477 >>> Name('validate_drp.PA1').has_spec
478 False
479 """
480 if self.spec is not None:
481 return True
482 else:
483 return False
485 @property
486 def has_metric(self):
487 """`True` if this object contains a metric name, either
488 relative or fully-qualified (`bool`).
490 >>> Name('validate_drp.PA1').has_metric
491 True
492 >>> Name(spec='design').has_metric
493 False
494 """
495 if self.metric is not None:
496 return True
497 else:
498 return False
500 @property
501 def has_relative(self):
502 """`True` if a relative specification name can be formed from this
503 object, i.e., `metric` and `spec` attributes are set (`bool`).
504 """
505 if self.is_spec and self.has_metric:
506 return True
507 else:
508 return False
510 @property
511 def is_package(self):
512 """`True` if this object is a package name (`bool`).
514 >>> Name('validate_drp').is_package
515 True
516 >>> Name('validate_drp.PA1').is_package
517 False
518 """
519 if self.has_package and \
520 self.is_metric is False and \
521 self.is_spec is False:
522 return True
523 else:
524 return False
526 @property
527 def is_metric(self):
528 """`True` if this object is a metric name, either relative or
529 fully-qualified (`bool`).
531 >>> Name('validate_drp.PA1').is_metric
532 True
533 >>> Name('validate_drp.PA1.design').is_metric
534 False
535 """
536 if self.has_metric is True and self.has_spec is False:
537 return True
538 else:
539 return False
541 @property
542 def is_spec(self):
543 """`True` if this object is a specification name, either relative or
544 fully-qualified (`bool`).
546 >>> Name('validate_drp.PA1').is_spec
547 False
548 >>> Name('validate_drp.PA1.design').is_spec
549 True
550 """
551 if self.has_spec is True:
552 return True
553 else:
554 return False
556 @property
557 def is_fq(self):
558 """`True` if this object is a fully-qualified name of either a
559 package, metric or specification (`bool`).
561 Examples
562 --------
563 A fully-qualified package name:
565 >>> Name('validate_drp').is_fq
566 True
568 A fully-qualified metric name:
570 >>> Name('validate_drp.PA1').is_fq
571 True
573 A fully-qualified specification name:
575 >>> Name('validate_drp.PA1.design_gri').is_fq
576 True
577 """
578 if self.is_package:
579 # package names are by definition fully qualified
580 return True
581 elif self.is_metric and self.has_package:
582 # fully-qualified metric
583 return True
584 elif self.is_spec and self.has_package and self.has_metric:
585 # fully-qualified specification
586 return True
587 else:
588 return False
590 @property
591 def is_relative(self):
592 """`True` if this object is a specification name that's not
593 fully-qualified, but is relative to a metric name (`bool`).
594 relative to a base name. (`bool`).
596 Package and metric names are never relative.
598 A relative specification name:
600 >>> Name(spec='PA1.design_gri').is_relative
601 True
603 But not:
605 >>> Name('validate_drp.PA1.design_gri').is_relative
606 False
607 """
608 if self.is_spec and \
609 self.has_metric is True and \
610 self.has_package is False:
611 return True
612 else:
613 return False
615 def __repr__(self):
616 if self.is_package:
617 return 'Name({self.package!r})'.format(self=self)
618 elif self.is_metric and not self.is_fq:
619 return 'Name(metric={self.metric!r})'.format(self=self)
620 elif self.is_metric and self.is_fq:
621 return 'Name({self.package!r}, {self.metric!r})'.format(
622 self=self)
623 elif self.is_spec and not self.is_fq and not self.is_relative:
624 return 'Name(spec={self.spec!r})'.format(
625 self=self)
626 elif self.is_spec and not self.is_fq and self.is_relative:
627 return 'Name(metric={self.metric!r}, spec={self.spec!r})'.format(
628 self=self)
629 else:
630 # Should be a fully-qualified specification
631 template = 'Name({self.package!r}, {self.metric!r}, {self.spec!r})'
632 return template.format(self=self)
634 def __str__(self):
635 """Canonical string representation of a Name (`str`).
637 Examples:
639 >>> str(Name(package='validate_drp'))
640 'validate_drp'
641 >>> str(Name(package='validate_drp', metric='PA1'))
642 'validate_drp.PA1'
643 >>> str(Name(package='validate_drp', metric='PA1', spec='design'))
644 'validate_drp.PA1.design'
645 >>> str(Name(metric='PA1', spec='design'))
646 'PA1.design'
647 """
648 if self.is_package:
649 return self.package
650 elif self.is_metric and not self.is_fq:
651 return self.metric
652 elif self.is_metric and self.is_fq:
653 return '{self.package}.{self.metric}'.format(self=self)
654 elif self.is_spec and not self.is_fq and not self.is_relative:
655 return self.spec
656 elif self.is_spec and not self.is_fq and self.is_relative:
657 return '{self.metric}.{self.spec}'.format(self=self)
658 else:
659 # Should be a fully-qualified specification
660 return '{self.package}.{self.metric}.{self.spec}'.format(
661 self=self)
663 @property
664 def fqn(self):
665 """The fully-qualified name (`str`).
667 Raises
668 ------
669 AttributeError
670 If the name is not a fully-qualified name (check `is_fq`)
672 Examples
673 --------
674 >>> Name('validate_drp', 'PA1').fqn
675 'validate_drp.PA1'
676 >>> Name('validate_drp', 'PA1', 'design').fqn
677 'validate_drp.PA1.design'
678 """
679 if self.is_fq:
680 return str(self)
681 else:
682 message = '{self!r} is not a fully-qualified name'
683 raise AttributeError(message.format(self=self))
685 @property
686 def relative_name(self):
687 """The relative specification name (`str`).
689 Raises
690 ------
691 AttributeError
692 If the object does not represent a specification, or if a relative
693 name cannot be formed because the `metric` is None.
695 Examples
696 --------
697 >>> Name('validate_drp.PA1.design').relative_name
698 'PA1.design'
699 """
700 if self.has_relative:
701 return '{self.metric}.{self.spec}'.format(self=self)
702 else:
703 message = '{self!r} is not a relative specification name'
704 raise AttributeError(message.format(self=self))