Coverage for python/lsst/verify/naming.py: 14%

244 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2022-11-08 22:32 -0800

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""" 

24 

25__all__ = ['Name'] 

26 

27 

28class Name(object): 

29 r"""Semantic name of a package, `~lsst.verify.Metric` or 

30 `~lsst.verify.Specification` in the `lsst.verify` framework. 

31 

32 ``Name`` instances are immutable and can be used as keys in mappings. 

33 

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

40 

41 The ``package`` field can also be fully specified: 

42 

43 >>> Name(package='validate_drp.PA1.design_gri') 

44 Name('validate_drp', 'PA1', 'design_gri') 

45 

46 Or the ``package`` field can be used as the sole positional argument: 

47 

48 >>> Name('validate_drp.PA1.design_gri') 

49 Name('validate_drp', 'PA1', 'design_gri') 

50 

51 metric : `str` or `Name` 

52 Name of the metric. The name can be relative (``'PA'``) or 

53 fully-specified (``'validate_drp.PA1'``). 

54 

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'``) 

59 

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

65 

66 Notes 

67 ----- 

68 Names in the Verification Framework are formatted as:: 

69 

70 package.metric.specification 

71 

72 A **fully-qualified name** is one that has all components included. For 

73 example, a fully specified metric name is:: 

74 

75 validate_drp.PA1 

76 

77 This means: "the ``PA1`` metric in the ``validate_drp`` package. 

78 

79 An example of a fully-specificed *specification* name:: 

80 

81 validate_drp.PA1.design 

82 

83 This means: "the ``design`` specification of the ``PA1`` metric in the 

84 ``validate_drp`` package. 

85 

86 A **relative name** is one that's missing a component. For example:: 

87 

88 PA1.design 

89 

90 Asserting this is a relative specification name, the package is not known. 

91 

92 Examples 

93 -------- 

94 

95 **Creation** 

96 

97 There are many patterns for creating Name instances. Different patterns 

98 are useful in different circumstances. 

99 

100 You can create a metric name from its components: 

101 

102 >>> Name(package='validate_drp', metric='PA1') 

103 Name('validate_drp', 'PA1') 

104 

105 Or a specification name from components: 

106 

107 >>> Name(package='validate_drp', metric='PA1', spec='design') 

108 Name('validate_drp', 'PA1', 'design') 

109 

110 You can equivalently create ``Name``\ s from fully-qualified strings: 

111 

112 >>> Name('validate_drp.PA1') 

113 Name('validate_drp', 'PA1') 

114 

115 >>> Name('validate_drp.PA1.design') 

116 Name('validate_drp', 'PA1', 'design') 

117 

118 You can also use an existing name to specify some components of the name: 

119 

120 >>> metric_name = Name('validate_drp.PA1') 

121 >>> Name(metric=metric_name, spec='design') 

122 Name('validate_drp', 'PA1', 'design') 

123 

124 A `TypeError` is raised if any components of the input names conflict. 

125 

126 Fully-specified metric names can be mixed with a ``spec`` component: 

127 

128 >>> Name(metric='validate_drp.PA1', spec='design') 

129 Name('validate_drp', 'PA1', 'design') 

130 

131 **String representation** 

132 

133 Converting a ``Name`` into a `str` gives you the canonical string 

134 representation, as fully-specified as possible: 

135 

136 >>> str(Name('validate_drp', 'PA1', 'design')) 

137 'validate_drp.PA1.design' 

138 

139 Alternatively, obtain the fully-qualified metric name from the 

140 `Name.fqn` property: 

141 

142 >>> name = Name('validate_drp', 'PA1', 'design') 

143 >>> name.fqn 

144 'validate_drp.PA1.design' 

145 

146 The relative name of a specification omits the package component: 

147 

148 >>> name = Name('validate_drp.PA1.design') 

149 >>> name.relative_name 

150 'PA1.design' 

151 """ 

152 

153 def __init__(self, package=None, metric=None, spec=None): 

154 self._package = None 

155 self._metric = None 

156 self._spec = None 

157 

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

171 

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

189 

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) 

194 

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

211 

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) 

216 

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

224 

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

234 

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

244 

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

254 

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

273 

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

288 

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

306 

307 @property 

308 def package(self): 

309 """Package name (`str`). 

310 

311 >>> name = Name('validate_drp.PA1.design') 

312 >>> name.package 

313 'validate_drp' 

314 """ 

315 return self._package 

316 

317 @property 

318 def metric(self): 

319 """Metric name (`str`). 

320 

321 >>> name = Name('validate_drp.PA1.design') 

322 >>> name.metric 

323 'PA1' 

324 """ 

325 return self._metric 

326 

327 @property 

328 def spec(self): 

329 """Specification name (`str`). 

330 

331 >>> name = Name('validate_drp.PA1.design') 

332 >>> name.spec 

333 'design' 

334 """ 

335 return self._spec 

336 

337 def __eq__(self, other): 

338 """Test equality of two specifications. 

339 

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) 

350 

351 def __ne__(self, other): 

352 return not self.__eq__(other) 

353 

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 

361 

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 

367 

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 

373 

374 # They're equal 

375 return False 

376 

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 

384 

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 

390 

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 

396 

397 # They're equal 

398 return False 

399 

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) 

406 

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) 

413 

414 def __hash__(self): 

415 return hash((self.package, self.metric, self.spec)) 

416 

417 def __contains__(self, name): 

418 """Test if another Name is contained by this Name. 

419 

420 A specification can be in a metric and a package. A metric can be in 

421 a package. 

422 

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 

434 

435 if self.is_package: 

436 if name.is_package: 

437 contains = False 

438 else: 

439 contains = self.package == name.package 

440 

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) 

447 

448 contains = contains and (self.metric == name.metric) 

449 

450 else: 

451 # Must be a specification, which cannot 'contain' anything 

452 contains = False 

453 

454 return contains 

455 

456 @property 

457 def has_package(self): 

458 """`True` if this object contains a package name (`bool`). 

459 

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 

469 

470 @property 

471 def has_spec(self): 

472 """`True` if this object contains a specification name, either 

473 relative or fully-qualified (`bool`). 

474 

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 

484 

485 @property 

486 def has_metric(self): 

487 """`True` if this object contains a metric name, either 

488 relative or fully-qualified (`bool`). 

489 

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 

499 

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 

509 

510 @property 

511 def is_package(self): 

512 """`True` if this object is a package name (`bool`). 

513 

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 

525 

526 @property 

527 def is_metric(self): 

528 """`True` if this object is a metric name, either relative or 

529 fully-qualified (`bool`). 

530 

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 

540 

541 @property 

542 def is_spec(self): 

543 """`True` if this object is a specification name, either relative or 

544 fully-qualified (`bool`). 

545 

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 

555 

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`). 

560 

561 Examples 

562 -------- 

563 A fully-qualified package name: 

564 

565 >>> Name('validate_drp').is_fq 

566 True 

567 

568 A fully-qualified metric name: 

569 

570 >>> Name('validate_drp.PA1').is_fq 

571 True 

572 

573 A fully-qualified specification name: 

574 

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 

589 

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`). 

595 

596 Package and metric names are never relative. 

597 

598 A relative specification name: 

599 

600 >>> Name(spec='PA1.design_gri').is_relative 

601 True 

602 

603 But not: 

604 

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 

614 

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) 

633 

634 def __str__(self): 

635 """Canonical string representation of a Name (`str`). 

636 

637 Examples: 

638 

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) 

662 

663 @property 

664 def fqn(self): 

665 """The fully-qualified name (`str`). 

666 

667 Raises 

668 ------ 

669 AttributeError 

670 If the name is not a fully-qualified name (check `is_fq`) 

671 

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

684 

685 @property 

686 def relative_name(self): 

687 """The relative specification name (`str`). 

688 

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. 

694 

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