Coverage for python/lsst/daf/base/propertyContainer/propertyContainerContinued.py: 93%

398 statements  

« prev     ^ index     » next       coverage.py v7.17.0, created at 2026-10-30 08:39 +0000

1# 

2# LSST Data Management System 

3# 

4# Copyright 2008-2017 AURA/LSST. 

5# 

6# This product includes software developed by the 

7# LSST Project (http://www.lsst.org/). 

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 LSST License Statement and 

20# the GNU General Public License along with this program. If not, 

21# see <http://www.lsstcorp.org/LegalNotices/>. 

22# 

23 

24__all__ = ["getPropertySetState", "getPropertyListState", "setPropertySetState", "setPropertyListState"] 

25 

26import enum 

27import math 

28import numbers 

29import warnings 

30import dataclasses 

31from collections.abc import Mapping, KeysView, ValuesView, ItemsView 

32from typing import TypeAlias, Union 

33 

34import astropy.io.fits 

35import astropy.io.fits.verify 

36 

37# Ensure that C++ exceptions are properly translated to Python 

38import lsst.pex.exceptions # noqa: F401 

39from lsst.utils import continueClass 

40 

41from .._dafBaseLib import PropertySet, PropertyList 

42from ..dateTime import DateTime 

43 

44 

45# Note that '|' syntax for unions doesn't work when we have to use a string 

46# literal (and we do since it's recursive and not an annotation). 

47NestedMetadataDict: TypeAlias = Mapping[str, Union[str, float, int, bool, "NestedMetadataDict"]] 

48 

49 

50# Map the type names to the internal type representation. 

51_TYPE_MAP = {} 

52for checkType in ("Bool", "Short", "Int", "Long", "LongLong", "UnsignedLongLong", 

53 "Float", "Double", "String", "DateTime", 

54 "PropertySet", "Undef"): 

55 type_obj = getattr(PropertySet, "TYPE_" + checkType) 

56 _TYPE_MAP[type_obj] = checkType 

57 # Store both directions. 

58 _TYPE_MAP[checkType] = type_obj 

59 

60# FITS headers cannot represent non-finite floats, so afw's FITS writer stores 

61# them as these strings and its reader converts them back to floats. 

62_NON_FINITE_FLOAT_STRINGS = {"NAN": math.nan, "+INFINITY": math.inf, "-INFINITY": -math.inf} 

63 

64# Keywords whose cards hold free text rather than a value. 

65_COMMENTARY_KEYWORDS = frozenset({"COMMENT", "HISTORY"}) 

66 

67 

68def _nonFiniteFloatToString(value): 

69 """Return the afw FITS string representation of a non-finite float, 

70 or `None` if the value is finite. 

71 """ 

72 if math.isfinite(value): 

73 return None 

74 if math.isnan(value): 

75 return "NAN" 

76 return "-INFINITY" if value < 0 else "+INFINITY" 

77 

78 

79def getPropertySetState(container, asLists=False): 

80 """Get the state of a PropertySet in a form that can be pickled. 

81 

82 Parameters 

83 ---------- 

84 container : `PropertySet` 

85 The property container. 

86 asLists : `bool`, optional 

87 If False, the default, `tuple` will be used for the contents. If true 

88 a `list` will be used. 

89 

90 Returns 

91 ------- 

92 state : `list` of `tuple` or `list` of `list` 

93 The state, as a list of tuples (or lists), each of which contains 

94 the following 3 items: 

95 

96 name (a `str`) 

97 the name of the item 

98 elementTypeName (a `str`) 

99 the suffix of a ``setX`` method name 

100 which is appropriate for the data type. For example integer 

101 data has ``elementTypeName="Int"` which corresponds to 

102 the ``setInt`` method. 

103 value 

104 the data for the item, in a form compatible 

105 with the set method named by ``elementTypeName`` 

106 """ 

107 names = container.names(topLevelOnly=True) 

108 sequence = list if asLists else tuple 

109 return [sequence((name, _propertyContainerElementTypeName(container, name), 

110 _propertyContainerGet(container, name, returnStyle=ReturnStyle.AUTO))) 

111 for name in names] 

112 

113 

114def getPropertyListState(container, asLists=False): 

115 """Get the state of a PropertyList in a form that can be pickled. 

116 

117 Parameters 

118 ---------- 

119 container : `PropertyList` 

120 The property container. 

121 asLists : `bool`, optional 

122 If False, the default, `tuple` will be used for the contents. If true 

123 a `list` will be used. 

124 

125 Returns 

126 ------- 

127 state : `list` of `tuple` or `list` of `list` 

128 The state, as a list of tuples (or lists), each of which contains 

129 the following 4 items: 

130 

131 name (a `str`): 

132 the name of the item 

133 elementTypeName (a `str`): 

134 the suffix of a ``setX`` method name 

135 which is appropriate for the data type. For example integer 

136 data has ``elementTypeName="Int"` which corresponds to 

137 the ``setInt`` method. 

138 value 

139 the data for the item, in a form compatible 

140 with the set method named by ``elementTypeName`` 

141 comment (a `str`): the comment. This item is only present 

142 if ``container`` is a PropertyList. 

143 """ 

144 sequence = list if asLists else tuple 

145 return [sequence((name, _propertyContainerElementTypeName(container, name), 

146 _propertyContainerGet(container, name, returnStyle=ReturnStyle.AUTO), 

147 container.getComment(name))) 

148 for name in container.getOrderedNames()] 

149 

150 

151def setPropertySetState(container, state): 

152 """Restore the state of a PropertySet, in place. 

153 

154 Parameters 

155 ---------- 

156 container : `PropertySet` 

157 The property container whose state is to be restored. 

158 It should be empty to start with and is updated in place. 

159 state : `list` 

160 The state, as returned by `getPropertySetState` 

161 """ 

162 for name, elemType, value in state: 

163 if elemType is not None: 163 ↛ 166line 163 didn't jump to line 166 because the condition on line 163 was always true

164 getattr(container, "set" + elemType)(name, value) 

165 else: 

166 raise ValueError(f"Unrecognized values for state restoration: ({name}, {elemType}, {value})") 

167 

168 

169def setPropertyListState(container, state): 

170 """Restore the state of a PropertyList, in place. 

171 

172 Parameters 

173 ---------- 

174 container : `PropertyList` 

175 The property container whose state is to be restored. 

176 It should be empty to start with and is updated in place. 

177 state : `list` 

178 The state, as returned by ``getPropertyListState`` 

179 """ 

180 for name, elemType, value, comment in state: 

181 getattr(container, "set" + elemType)(name, value, comment) 

182 

183 

184class ReturnStyle(enum.Enum): 

185 ARRAY = enum.auto() 

186 SCALAR = enum.auto() 

187 AUTO = enum.auto() 

188 

189 

190def _propertyContainerElementTypeName(container, name): 

191 """Return name of the type of a particular element 

192 

193 Parameters 

194 ---------- 

195 container : `lsst.daf.base.PropertySet` or `lsst.daf.base.PropertyList` 

196 Container including the element 

197 name : `str` 

198 Name of element 

199 """ 

200 try: 

201 t = container.typeOf(name) 

202 except LookupError as e: 

203 # KeyError is more commonly expected when asking for an element 

204 # from a mapping. 

205 raise KeyError(str(e)) from None 

206 

207 return _TYPE_MAP.get(t, None) 

208 

209 

210def _propertyContainerGet(container, name, returnStyle): 

211 """Get a value of unknown type as a scalar or array 

212 

213 Parameters 

214 ---------- 

215 container : `lsst.daf.base.PropertySet` or `lsst.daf.base.PropertyList` 

216 Container from which to get the value 

217 name : `str` 

218 Name of item 

219 returnStyle : `ReturnStyle` 

220 Control whether numeric or string data is returned as an array 

221 or scalar (the other types, ``PropertyList``, ``PropertySet`` 

222 and ``PersistablePtr``, are always returned as a scalar): 

223 - ReturnStyle.ARRAY: return numeric or string data types 

224 as an array of values. 

225 - ReturnStyle.SCALAR: return numeric or string data types 

226 as a single value; if the item has multiple values then 

227 return the last value. 

228 - ReturnStyle.AUTO: (deprecated) return numeric or string data 

229 as a scalar if there is just one item, or as an array 

230 otherwise. 

231 

232 Raises 

233 ------ 

234 KeyError 

235 Raised if the specified key does not exist in the container. 

236 TypeError 

237 Raised if the value retrieved is of an unexpected type. 

238 ValueError 

239 Raised if the value for ``returnStyle`` is not correct. 

240 """ 

241 if not container.exists(name): 

242 raise KeyError(name + " not found") 

243 if returnStyle not in ReturnStyle: 243 ↛ 244line 243 didn't jump to line 244 because the condition on line 243 was never true

244 raise ValueError("returnStyle {} must be a ReturnStyle".format(returnStyle)) 

245 

246 elemType = _propertyContainerElementTypeName(container, name) 

247 if elemType and elemType != "PropertySet": 

248 value = getattr(container, "getArray" + elemType)(name) 

249 if returnStyle == ReturnStyle.ARRAY or (returnStyle == ReturnStyle.AUTO and len(value) > 1): 

250 return value 

251 return value[-1] 

252 

253 if container.isPropertySetPtr(name): 253 ↛ 258line 253 didn't jump to line 258 because the condition on line 253 was always true

254 try: 

255 return container.getAsPropertyListPtr(name) 

256 except Exception: 

257 return container.getAsPropertySetPtr(name) 

258 try: 

259 return container.getAsPersistablePtr(name) 

260 except Exception: 

261 pass 

262 raise TypeError('Unknown PropertySet value type for ' + name) 

263 

264 

265def _iterable(a): 

266 """Make input iterable. 

267 

268 Takes whatever is given to it and yields it back one element at a time. 

269 If it is not an iterable or it is a string or PropertySet/List, 

270 yields itself. 

271 """ 

272 if isinstance(a, (str, PropertyList, PropertySet)): 

273 yield a 

274 return 

275 try: 

276 yield from a 

277 except Exception: 

278 yield a 

279 

280 

281def _guessIntegerType(container, name, value): 

282 """Given an existing container and name, determine the type 

283 that should be used for the supplied value. The supplied value 

284 is assumed to be a scalar. 

285 

286 On Python 3 all ints are LongLong but we need to be able to store them 

287 in Int containers if that is what is being used (testing for truncation). 

288 Int is assumed to mean 32bit integer (2147483647 to -2147483648). 

289 

290 If there is no pre-existing value we have to decide what to do. For now 

291 we pick Int if the value is less than maxsize. 

292 

293 Parameters 

294 ---------- 

295 container : `lsst.daf.base.PropertySet` or `lsst.daf.base.PropertyList` 

296 Container from which to get the value 

297 

298 name : `str` 

299 Name of item 

300 

301 value : `object` 

302 Value to be assigned a type. Can be an iterable. 

303 

304 Returns 

305 ------- 

306 useType : `str` or none 

307 Type to use for the supplied value. `None` if the input is 

308 `bool` or a non-integral value. 

309 """ 

310 maxInt = 2147483647 

311 minInt = -2147483648 

312 maxLongLong = 2**63 - 1 

313 minLongLong = -2**63 

314 maxU64 = 2**64 - 1 

315 minU64 = 0 

316 

317 # Go through the values to find the range of supplied integers, 

318 # stopping early if we don't have an integer. 

319 min = None 

320 max = None 

321 for v in _iterable(value): 

322 # Do not try to convert a bool to an integer 

323 if not isinstance(v, numbers.Integral) or isinstance(v, bool): 

324 return None 

325 

326 if min is None: 

327 min = v 

328 max = v 

329 elif v < min: 

330 min = v 

331 elif v > max: 

332 max = v 

333 

334 # Safety net 

335 if min is None or max is None: 335 ↛ 336line 335 didn't jump to line 336 because the condition on line 335 was never true

336 raise RuntimeError(f"Internal logic failure calculating integer range of {value}") 

337 

338 def _choose_int_from_range(int_value, current_type): 

339 # If this is changing type from non-integer the current type 

340 # does not matter. 

341 if current_type not in {"Int", "LongLong", "UnsignedLongLong"}: 

342 current_type = None 

343 

344 if int_value <= maxInt and int_value >= minInt and current_type in (None, "Int"): 

345 # Use Int only if in range and either no current type or the 

346 # current type is an Int. 

347 use_type = "Int" 

348 elif int_value >= minLongLong and int_value < 0: 

349 # All large negatives must be LongLong if they did not fit 

350 # in Int clause above. 

351 use_type = "LongLong" 

352 elif int_value >= 0 and int_value <= maxLongLong and current_type in (None, "Int", "LongLong"): 

353 # Larger than Int or already a LongLong 

354 use_type = "LongLong" 

355 elif int_value <= maxU64 and int_value >= minU64: 

356 use_type = "UnsignedLongLong" 

357 else: 

358 raise RuntimeError("Unable to guess integer type for storing out of " 

359 f"range value: {int_value}") 

360 return use_type 

361 

362 if container.exists(name): 

363 containerType = _propertyContainerElementTypeName(container, name) 

364 else: 

365 containerType = None 

366 

367 useTypeMin = _choose_int_from_range(min, containerType) 

368 useTypeMax = _choose_int_from_range(max, containerType) 

369 

370 if useTypeMin == useTypeMax: 

371 return useTypeMin 

372 

373 # When different the combinations are: 

374 # Int + LongLong 

375 # Int + UnsignedLongLong 

376 # LongLong + UnsignedLongLong 

377 

378 choices = {useTypeMin, useTypeMax} 

379 if choices == {"Int", "LongLong"}: 

380 return "LongLong" 

381 

382 # If UnsignedLongLong is required things will break if the min 

383 # is negative. They will break whatever we choose if that is the case 

384 # but we have no choice but to return the UnsignedLongLong regardless. 

385 if "UnsignedLongLong" in choices: 385 ↛ 388line 385 didn't jump to line 388 because the condition on line 385 was always true

386 return "UnsignedLongLong" 

387 

388 raise RuntimeError(f"Logic error in guessing integer type from {min} and {max}") 

389 

390 

391def _propertyContainerSet(container, name, value, typeMenu, *args): 

392 """Set a single Python value of unknown type 

393 """ 

394 try: 

395 exemplar = next(_iterable(value)) 

396 except StopIteration: 

397 # Do nothing if nothing provided. This matches the behavior 

398 # of the explicit setX() methods. 

399 return 

400 t = type(exemplar) 

401 setType = _guessIntegerType(container, name, value) 

402 

403 if setType is not None or t in typeMenu: 

404 if setType is None: 

405 setType = typeMenu[t] 

406 return getattr(container, "set" + setType)(name, value, *args) 

407 # Allow for subclasses 

408 for checkType in typeMenu: 408 ↛ 412line 408 didn't jump to line 412 because the loop on line 408 didn't complete

409 if (checkType is None and exemplar is None) or \ 

410 (checkType is not None and isinstance(exemplar, checkType)): 

411 return getattr(container, "set" + typeMenu[checkType])(name, value, *args) 

412 raise TypeError("Unknown value type for key '%s': %s" % (name, t)) 

413 

414 

415def _propertyContainerAdd(container, name, value, typeMenu, *args): 

416 """Add a single Python value of unknown type 

417 """ 

418 try: 

419 exemplar = next(_iterable(value)) 

420 except StopIteration: 

421 # Adding an empty iterable to an existing entry is a no-op 

422 # since there is nothing to add. 

423 return 

424 t = type(exemplar) 

425 addType = _guessIntegerType(container, name, exemplar) 

426 

427 if addType is not None or t in typeMenu: 

428 if addType is None: 

429 addType = typeMenu[t] 

430 return getattr(container, "add" + addType)(name, value, *args) 

431 # Allow for subclasses 

432 for checkType in typeMenu: 432 ↛ 436line 432 didn't jump to line 436 because the loop on line 432 didn't complete

433 if (checkType is None and exemplar is None) or \ 

434 (checkType is not None and isinstance(exemplar, checkType)): 

435 return getattr(container, "add" + typeMenu[checkType])(name, value, *args) 

436 raise TypeError("Unknown value type for key '%s': %s" % (name, t)) 

437 

438 

439def _makePropertySet(state): 

440 """Make a `PropertySet` from the state returned by `getPropertySetState` 

441 

442 Parameters 

443 ---------- 

444 state : `list` 

445 The data returned by `getPropertySetState`. 

446 """ 

447 ps = PropertySet() 

448 setPropertySetState(ps, state) 

449 return ps 

450 

451 

452def _makePropertyList(state): 

453 """Make a `PropertyList` from the state returned by 

454 `getPropertyListState` 

455 

456 Parameters 

457 ---------- 

458 state : `list` 

459 The data returned by `getPropertySetState`. 

460 """ 

461 pl = PropertyList() 

462 setPropertyListState(pl, state) 

463 return pl 

464 

465 

466@continueClass 

467class PropertySet: 

468 # Mapping of type to method names; 

469 # int types are omitted due to use of _guessIntegerType 

470 _typeMenu = {bool: "Bool", 

471 float: "Double", 

472 str: "String", 

473 DateTime: "DateTime", 

474 PropertySet: "PropertySet", 

475 PropertyList: "PropertySet", 

476 None: "Undef", 

477 } 

478 

479 @classmethod 

480 def from_mapping(cls, metadata): 

481 """Create a `PropertySet` from a mapping or dict-like object. 

482 

483 Parameters 

484 ---------- 

485 metadata : `collections.abc.Mapping` 

486 Metadata from which to create the `PropertySet`. 

487 Can be a mapping, a `~dataclasses.dataclass` or anything that 

488 supports ``toDict()``, ``to_dict()`` or ``dict()`` method. 

489 It is assumed that the dictionary is expanded recursively by these 

490 methods or that the Python type can be understood by `PropertySet`. 

491 

492 Returns 

493 ------- 

494 ps : `PropertySet` 

495 The new `PropertySet`. 

496 """ 

497 ps = cls() 

498 d = None 

499 if isinstance(metadata, Mapping): 

500 d = metadata 

501 elif dataclasses.is_dataclass(metadata): 

502 d = dataclasses.asdict(metadata) 

503 else: 

504 for attr in ("to_dict", "toDict", "dict"): 

505 if hasattr(metadata, attr): 

506 d = getattr(metadata, attr)() 

507 break 

508 if d is None: 

509 raise ValueError("Unable to extract mappings from the supplied metadata of type" 

510 f" {type(metadata)}") 

511 ps.update(d) 

512 return ps 

513 

514 def get(self, name, default=None): 

515 """Return an item as a scalar, else default. 

516 

517 Identical to `getScalar` except that a default value is returned 

518 if the requested key is not present. If an array item is requested 

519 the final value in the array will be returned. 

520 

521 Parameters 

522 ---------- 

523 name : `str` 

524 Name of item 

525 default : `object`, optional 

526 Default value to use if the named item is not present. 

527 

528 Returns 

529 ------- 

530 value : any type supported by container 

531 Single value of any type supported by the container, else the 

532 default value if the requested item is not present in the 

533 container. For array items the most recently added value is 

534 returned. 

535 """ 

536 try: 

537 return _propertyContainerGet(self, name, returnStyle=ReturnStyle.SCALAR) 

538 except KeyError: 

539 return default 

540 

541 def getArray(self, name): 

542 """Return an item as an array if the item is numeric or string 

543 

544 If the item is a `PropertySet`, `PropertyList` or 

545 `lsst.daf.base.PersistablePtr` then return the item as a scalar. 

546 

547 Parameters 

548 ---------- 

549 name : `str` 

550 Name of item 

551 

552 Returns 

553 ------- 

554 values : `list` of any type supported by container 

555 The contents of the item, guaranteed to be returned as a `list.` 

556 

557 Raises 

558 ------ 

559 KeyError 

560 Raised if the item does not exist. 

561 """ 

562 return _propertyContainerGet(self, name, returnStyle=ReturnStyle.ARRAY) 

563 

564 def getScalar(self, name): 

565 """Return an item as a scalar 

566 

567 If the item has more than one value then the last value is returned. 

568 

569 Parameters 

570 ---------- 

571 name : `str` 

572 Name of item 

573 

574 Returns 

575 ------- 

576 value : scalar item 

577 Value stored in the item. If the item refers to an array the 

578 most recently added value is returned. 

579 

580 Raises 

581 ------ 

582 KeyError 

583 Raised if the item does not exist. 

584 """ 

585 return _propertyContainerGet(self, name, returnStyle=ReturnStyle.SCALAR) 

586 

587 def set(self, name, value): 

588 """Set the value of an item 

589 

590 If the item already exists it is silently replaced; the types 

591 need not match. 

592 

593 Parameters 

594 ---------- 

595 name : `str` 

596 Name of item 

597 value : any supported type 

598 Value of item; may be a scalar or array 

599 """ 

600 return _propertyContainerSet(self, name, value, self._typeMenu) 

601 

602 def add(self, name, value): 

603 """Append one or more values to a given item, which need not exist 

604 

605 If the item exists then the new value(s) are appended; 

606 otherwise it is like calling `set` 

607 

608 Parameters 

609 ---------- 

610 name : `str` 

611 Name of item 

612 value : any supported type 

613 Value of item; may be a scalar or array 

614 

615 Notes 

616 ----- 

617 If ``value`` is an `lsst.daf.base.PropertySet` or 

618 `lsst.daf.base.PropertyList` then ``value`` replaces 

619 the existing value. Also the item is added as a live 

620 reference, so updating ``value`` will update this container 

621 and vice-versa. 

622 

623 Raises 

624 ------ 

625 lsst::pex::exceptions::TypeError 

626 Raised if the type of `value` is incompatible with the existing 

627 value of the item. 

628 """ 

629 return _propertyContainerAdd(self, name, value, self._typeMenu) 

630 

631 def update(self, addition): 

632 """Update the current container with the supplied additions. 

633 

634 Parameters 

635 ---------- 

636 addition : `collections.abc.Mapping` or `PropertySet` 

637 The content to merge into the current container. 

638 

639 Notes 

640 ----- 

641 This is not the same as calling `PropertySet.combine` since the 

642 behavior differs when both mappings contain the same key. This 

643 method updates by overwriting existing values completely with 

644 the new value. 

645 """ 

646 if isinstance(addition, PropertySet): 

647 # To support array values we can not use the dict interface 

648 # and instead use the copy() method which overwrites 

649 for k in addition: 

650 self.copy(k, addition, k) 

651 else: 

652 for k, v in addition.items(): 

653 self[k] = v 

654 

655 def toDict(self): 

656 """Returns a (possibly nested) dictionary with all properties. 

657 

658 Returns 

659 ------- 

660 d : `dict` 

661 Dictionary with all names and values (no comments). 

662 """ 

663 

664 d = {} 

665 for name in self.names(): 

666 v = _propertyContainerGet(self, name, returnStyle=ReturnStyle.AUTO) 

667 

668 if isinstance(v, PropertySet): 

669 d[name] = PropertySet.toDict(v) 

670 else: 

671 d[name] = v 

672 return d 

673 

674 def __eq__(self, other): 

675 if type(self) is not type(other): 675 ↛ 676line 675 didn't jump to line 676 because the condition on line 675 was never true

676 return NotImplemented 

677 

678 if len(self) != len(other): 678 ↛ 679line 678 didn't jump to line 679 because the condition on line 678 was never true

679 return False 

680 

681 for name in self: 

682 if (self_typeOf := self.typeOf(name)) != other.typeOf(name): 682 ↛ 683line 682 didn't jump to line 683 because the condition on line 682 was never true

683 return False 

684 

685 if (v1 := _propertyContainerGet(self, name, returnStyle=ReturnStyle.AUTO)) != \ 

686 (v2 := _propertyContainerGet(other, name, returnStyle=ReturnStyle.AUTO)): 

687 # It is possible that we have floats that are NaN. When 

688 # equating two PropertySets if there are fields with NaN 

689 # these should equate equal. 

690 if self_typeOf in (_TYPE_MAP["Float"], _TYPE_MAP["Double"]) \ 690 ↛ 692line 690 didn't jump to line 692 because the condition on line 690 was never true

691 and math.isnan(v1) and math.isnan(v2): 

692 pass 

693 else: 

694 return False 

695 

696 return True 

697 

698 def __copy__(self): 

699 # Copy without having to go through pickle state 

700 ps = PropertySet() 

701 for itemName in self: 

702 ps.copy(itemName, self, itemName) 

703 return ps 

704 

705 def __deepcopy__(self, memo): 

706 result = self.deepCopy() 

707 memo[id(self)] = result 

708 return result 

709 

710 def __contains__(self, name): 

711 """Determines if the name is found at the top level hierarchy 

712 of the container. 

713 

714 Notes 

715 ------ 

716 Does not use `PropertySet.exists()`` because that includes support 

717 for "."-delimited names. This method is consistent with the 

718 items returned from ``__iter__``. 

719 """ 

720 return name in self.names(topLevelOnly=True) 

721 

722 def __setitem__(self, name, value): 

723 """Assigns the supplied value to the container. 

724 

725 Parameters 

726 ---------- 

727 name : `str` 

728 Name of item to update. 

729 value : Value to assign 

730 Can be any value supported by the container's ``set()`` 

731 method. `~collections.abc.Mapping` are converted to 

732 `PropertySet` before assignment. 

733 

734 Notes 

735 ----- 

736 Uses `PropertySet.set`, overwriting any previous value. 

737 """ 

738 if isinstance(value, Mapping): 

739 # Create a property set instead 

740 ps = PropertySet() 

741 for k, v in value.items(): 

742 ps[k] = v 

743 value = ps 

744 self.set(name, value) 

745 

746 def __getitem__(self, name): 

747 """Returns a scalar item from the container. 

748 

749 Notes 

750 ----- 

751 Uses `PropertySet.getScalar` to guarantee that a single value 

752 will be returned. 

753 """ 

754 return self.getScalar(name) 

755 

756 def __delitem__(self, name): 

757 if self.exists(name): 757 ↛ 761line 757 didn't jump to line 761 because the condition on line 757 was always true

758 # dot-delimited names should work so cannot use "in". 

759 self.remove(name) 

760 else: 

761 raise KeyError(f"{name} not present in dict") 

762 

763 def __str__(self): 

764 return self.toString() 

765 

766 def __len__(self): 

767 return self.nameCount(topLevelOnly=True) 

768 

769 def __iter__(self): 

770 for n in self.names(topLevelOnly=True): 

771 yield n 

772 

773 def keys(self): 

774 return KeysView(self) 

775 

776 def items(self): 

777 return ItemsView(self) 

778 

779 def values(self): 

780 return ValuesView(self) 

781 

782 def pop(self, name, default=None): 

783 """Remove the named key and return its value. 

784 

785 Parameters 

786 ---------- 

787 name : `str` 

788 Name of the key to remove. Can be hierarchical. 

789 default : Any, optional 

790 Value to return if the key is not present. 

791 

792 Returns 

793 ------- 

794 value : Any 

795 The value of the item as would be returned using `getScalar()`. 

796 

797 Raises 

798 ------ 

799 KeyError 

800 Raised if no default is given and the key is missing. 

801 """ 

802 if self.exists(name): 

803 value = self[name] 

804 self.remove(name) 

805 else: 

806 if default is None: 

807 raise KeyError(name) 

808 value = default 

809 return value 

810 

811 def __reduce__(self): 

812 # It would be a bit simpler to use __setstate__ and __getstate__. 

813 # However, implementing __setstate__ in Python causes segfaults 

814 # because pickle creates a new instance by calling 

815 # object.__new__(PropertyList, *args) which bypasses 

816 # the pybind11 memory allocation step. 

817 return (_makePropertySet, (getPropertySetState(self),)) 

818 

819 def get_dict(self, key: str) -> NestedMetadataDict: 

820 """Return a possibly-hierarchical nested `dict`. 

821 

822 This implements the `lsst.pipe.base.GetDictMetadata` protocol for 

823 consistency with `lsst.pipe.base.TaskMetadata` and `PropertyList`. 

824 

825 Parameters 

826 ---------- 

827 key : `str` 

828 String key associated with the mapping. May not have a ``.`` 

829 character. 

830 

831 Returns 

832 ------- 

833 value : `~collections.abc.Mapping` 

834 Possibly-nested mapping, with `str` keys and values that are `int`, 

835 `float`, `str`, `bool`, or another `dict` with the same key and 

836 value types. Will be empty if ``key`` does not exist. 

837 

838 Raises 

839 ------ 

840 TypeError 

841 Raised if the value associated with this key is not a nested 

842 dictionary, but does exist. Note that this behavior is not 

843 consistent with `PropertyList` (which returns an empty `dict`). 

844 """ 

845 if self.exists(key): 

846 return self.getScalar(key).toDict() 

847 else: 

848 return {} 

849 

850 def set_dict(self, key: str, value: NestedMetadataDict) -> None: 

851 """Assign a possibly-hierarchical nested `dict`. 

852 

853 This implements the `lsst.pipe.base.SetDictMetadata` protocol for 

854 consistency with `lsst.pipe.base.TaskMetadata` and `PropertyList`. 

855 

856 Parameters 

857 ---------- 

858 key : `str` 

859 String key associated with the mapping. May not have a ``.`` 

860 character. 

861 value : `~collections.abc.Mapping` 

862 Possibly-nested mapping, with `str` keys and values that are `int`, 

863 `float`, `str`, `bool`, or another `dict` with the same key and 

864 value types. May not have a ``.`` 

865 character. 

866 """ 

867 self.set(key, PropertySet.from_mapping(value)) 

868 

869 

870@continueClass 

871class PropertyList: 

872 # Mapping of type to method names 

873 _typeMenu = {bool: "Bool", 

874 int: "Int", 

875 float: "Double", 

876 str: "String", 

877 DateTime: "DateTime", 

878 PropertySet: "PropertySet", 

879 PropertyList: "PropertySet", 

880 None: "Undef", 

881 } 

882 

883 COMMENTSUFFIX = "#COMMENT" 

884 """Special suffix used to indicate that a named item being assigned 

885 using dict syntax is referring to a comment, not value.""" 

886 

887 def get(self, name, default=None): 

888 """Return an item as a scalar, else default. 

889 

890 Identical to `getScalar` except that a default value is returned 

891 if the requested key is not present. If an array item is requested 

892 the final value in the array will be returned. 

893 

894 Parameters 

895 ---------- 

896 name : ``str`` 

897 Name of item 

898 default : `object`, optional 

899 Default value to use if the named item is not present. 

900 

901 Returns 

902 ------- 

903 value : any type supported by container 

904 Single value of any type supported by the container, else the 

905 default value if the requested item is not present in the 

906 container. For array items the most recently added value is 

907 returned. 

908 """ 

909 try: 

910 return _propertyContainerGet(self, name, returnStyle=ReturnStyle.SCALAR) 

911 except KeyError: 

912 return default 

913 

914 def getArray(self, name): 

915 """Return an item as a list. 

916 

917 Parameters 

918 ---------- 

919 name : `str` 

920 Name of item 

921 

922 Returns 

923 ------- 

924 values : `list` of values 

925 The contents of the item, guaranteed to be returned as a `list.` 

926 

927 Raises 

928 ------ 

929 KeyError 

930 Raised if the item does not exist. 

931 """ 

932 return _propertyContainerGet(self, name, returnStyle=ReturnStyle.ARRAY) 

933 

934 def getScalar(self, name): 

935 """Return an item as a scalar 

936 

937 If the item has more than one value then the last value is returned. 

938 

939 Parameters 

940 ---------- 

941 name : `str` 

942 Name of item. 

943 

944 Returns 

945 ------- 

946 value : scalar item 

947 Value stored in the item. If the item refers to an array the 

948 most recently added value is returned. 

949 

950 Raises 

951 ------ 

952 KeyError 

953 Raised if the item does not exist. 

954 """ 

955 return _propertyContainerGet(self, name, returnStyle=ReturnStyle.SCALAR) 

956 

957 def set(self, name, value, comment=None): 

958 """Set the value of an item 

959 

960 If the item already exists it is silently replaced; the types 

961 need not match. 

962 

963 Parameters 

964 ---------- 

965 name : `str` 

966 Name of item 

967 value : any supported type 

968 Value of item; may be a scalar or array 

969 """ 

970 args = [] 

971 if comment is not None: 

972 args.append(comment) 

973 return _propertyContainerSet(self, name, value, self._typeMenu, *args) 

974 

975 def add(self, name, value, comment=None): 

976 """Append one or more values to a given item, which need not exist 

977 

978 If the item exists then the new value(s) are appended; 

979 otherwise it is like calling `set` 

980 

981 Parameters 

982 ---------- 

983 name : `str` 

984 Name of item 

985 value : any supported type 

986 Value of item; may be a scalar or array 

987 

988 Notes 

989 ----- 

990 If `value` is an `lsst.daf.base.PropertySet` items are added 

991 using dotted names (e.g. if name="a" and value contains 

992 an item "b" which is another PropertySet and contains an 

993 item "c" which is numeric or string, then the value of "c" 

994 is added as "a.b.c", appended to the existing values of 

995 "a.b.c" if any (in which case the types must be compatible). 

996 

997 Raises 

998 ------ 

999 lsst::pex::exceptions::TypeError 

1000 Raise if the type of ``value`` is incompatible with the existing 

1001 value of the item. 

1002 """ 

1003 args = [] 

1004 if comment is not None: 

1005 args.append(comment) 

1006 return _propertyContainerAdd(self, name, value, self._typeMenu, *args) 

1007 

1008 def setComment(self, name, comment): 

1009 """Set the comment for an existing entry. 

1010 

1011 Parameters 

1012 ---------- 

1013 name : `str` 

1014 Name of the key to receive updated comment. 

1015 comment : `comment` 

1016 New comment string. 

1017 """ 

1018 # The only way to do this is to replace the existing entry with 

1019 # one that has the new comment 

1020 containerType = _propertyContainerElementTypeName(self, name) 

1021 if self.isArray(name): 1021 ↛ 1022line 1021 didn't jump to line 1022 because the condition on line 1021 was never true

1022 value = self.getArray(name) 

1023 else: 

1024 value = self.getScalar(name) 

1025 getattr(self, f"set{containerType}")(name, value, comment) 

1026 

1027 @classmethod 

1028 def from_astropy_header(cls, header: astropy.io.fits.Header) -> "PropertyList": 

1029 """Convert an `astropy.io.fits.Header` into a `PropertyList`. 

1030 

1031 Parameters 

1032 ---------- 

1033 header : `astropy.io.fits.Header` 

1034 FITS header to convert. 

1035 

1036 Returns 

1037 ------- 

1038 propertyList : `PropertyList` 

1039 Equivalent legacy metadata. Blank (keyword-less) cards and 

1040 cards whose values Astropy cannot parse (e.g. from unterminated 

1041 quoted strings in real raw headers) are skipped. Cards with 

1042 `astropy.io.fits.card.Undefined` values (e.g. ``SEEING =``) are 

1043 stored as undefined properties (``TYPE_Undef``), preserving any 

1044 comment text. COMMENT and HISTORY cards hold their text as 

1045 string values, as they do both in FITS convention and when read 

1046 back by `lsst.afw.fits.readMetadata`. The string values 

1047 ``NAN``, ``+INFINITY`` and ``-INFINITY`` are converted to the 

1048 corresponding non-finite floats, matching 

1049 `lsst.afw.fits.readMetadata`. 

1050 

1051 Notes 

1052 ----- 

1053 Comments on cards are preserved via `PropertyList.add`, and so are 

1054 keyed by name: cards that repeat a keyword share that keyword's 

1055 comment slot. 

1056 """ 

1057 result = cls() 

1058 for card in header.cards: 

1059 if not card.keyword: 1059 ↛ 1061line 1059 didn't jump to line 1061 because the condition on line 1059 was never true

1060 # Truly blank cards have no name to key on. 

1061 continue 

1062 try: 

1063 value = card.value 

1064 except astropy.io.fits.verify.VerifyError: 

1065 # Values astropy cannot parse (e.g. from unterminated quoted 

1066 # strings) have no representation in legacy metadata. 

1067 continue 

1068 comment = card.comment or None 

1069 if isinstance(value, astropy.io.fits.card.Undefined): 

1070 value = None 

1071 elif isinstance(value, str) and card.keyword not in _COMMENTARY_KEYWORDS: 

1072 value = _NON_FINITE_FLOAT_STRINGS.get(value, value) 

1073 result.add(card.keyword, value, comment) 

1074 return result 

1075 

1076 def to_astropy_header(self) -> astropy.io.fits.Header: 

1077 """Convert this `PropertyList` into an `astropy.io.fits.Header`. 

1078 

1079 Returns 

1080 ------- 

1081 header : `astropy.io.fits.Header` 

1082 FITS header with one card per stored value, in insertion order. 

1083 Undefined properties become cards with no value 

1084 (`astropy.io.fits.card.Undefined`); string values stored under 

1085 the COMMENT and HISTORY keywords render as bare comment cards 

1086 (``COMMENT text``) rather than ``COMMENT = 'text'``. Comments 

1087 set via `PropertyList.add` or `setComment` are restored. 

1088 Non-finite floats, which FITS headers cannot represent, are 

1089 stored as the strings ``NAN``, ``+INFINITY`` and ``-INFINITY``, 

1090 matching the afw FITS writer. 

1091 """ 

1092 header = astropy.io.fits.Header() 

1093 with warnings.catch_warnings(): 

1094 # Silence warnings about long keys becoming HIERARCH and 

1095 # comments being truncated; both are unavoidable here. 

1096 warnings.simplefilter("ignore", category=astropy.io.fits.verify.VerifyWarning) 

1097 for name in self.getOrderedNames(): 

1098 values = self.getArray(name) 

1099 comment = self.getComment(name) or None 

1100 for value in values: 

1101 if isinstance(value, float) and (nonFinite := _nonFiniteFloatToString(value)): 

1102 value = nonFinite 

1103 header.append(astropy.io.fits.Card(name, value, comment), end=True) 

1104 return header 

1105 

1106 def toList(self): 

1107 """Return a list of tuples of name, value, comment for each property 

1108 in the order that they were inserted. 

1109 

1110 Returns 

1111 ------- 

1112 ret : `list` of `tuple` 

1113 Tuples of name, value, comment for each property in the order 

1114 in which they were inserted. 

1115 """ 

1116 orderedNames = self.getOrderedNames() 

1117 ret = [] 

1118 for name in orderedNames: 

1119 if self.isArray(name): 

1120 values = _propertyContainerGet(self, name, returnStyle=ReturnStyle.AUTO) 

1121 for v in values: 

1122 ret.append((name, v, self.getComment(name))) 

1123 else: 

1124 ret.append((name, _propertyContainerGet(self, name, returnStyle=ReturnStyle.AUTO), 

1125 self.getComment(name))) 

1126 return ret 

1127 

1128 def toOrderedDict(self): 

1129 """Return an ordered dictionary with all properties in the order that 

1130 they were inserted. 

1131 

1132 Returns 

1133 ------- 

1134 d : `dict` 

1135 Ordered dictionary with all properties in the order that they 

1136 were inserted. Comments are not included. 

1137 

1138 Notes 

1139 ----- 

1140 As of Python 3.6 dicts retain their insertion order. 

1141 """ 

1142 d = {} 

1143 for name in self: 

1144 d[name] = _propertyContainerGet(self, name, returnStyle=ReturnStyle.AUTO) 

1145 return d 

1146 

1147 # For PropertyList the two are equivalent 

1148 toDict = toOrderedDict 

1149 

1150 def __eq__(self, other): 

1151 # super() doesn't seem to work properly in @continueClass; 

1152 # note that super with arguments seems to work at first, but actually 

1153 # doesn't either. 

1154 if not PropertySet.__eq__(self, other): 1154 ↛ 1155line 1154 didn't jump to line 1155 because the condition on line 1154 was never true

1155 return False 

1156 

1157 for name in self: 

1158 if self.getComment(name) != other.getComment(name): 1158 ↛ 1159line 1158 didn't jump to line 1159 because the condition on line 1158 was never true

1159 return False 

1160 

1161 return True 

1162 

1163 def __copy__(self): 

1164 # Copy without having to go through pickle state 

1165 pl = PropertyList() 

1166 for itemName in self: 

1167 pl.copy(itemName, self, itemName) 

1168 return pl 

1169 

1170 def __deepcopy__(self, memo): 

1171 result = self.deepCopy() 

1172 memo[id(self)] = result 

1173 return result 

1174 

1175 def __iter__(self): 

1176 for n in self.getOrderedNames(): 

1177 yield n 

1178 

1179 def __setitem__(self, name, value): 

1180 """Assigns the supplied value to the container. 

1181 

1182 Parameters 

1183 ---------- 

1184 name : `str` 

1185 Name of item to update. If the name ends with 

1186 `PropertyList.COMMENTSUFFIX`, the comment is updated rather 

1187 than the value. 

1188 value : Value to assign 

1189 Can be any value supported by the container's ``set()`` 

1190 method. `~collections.abc.Mapping` are converted to 

1191 `PropertySet` before assignment. 

1192 

1193 Notes 

1194 ----- 

1195 Uses `PropertySet.set`, overwriting any previous value. 

1196 """ 

1197 if name.endswith(self.COMMENTSUFFIX): 

1198 name = name[:-len(self.COMMENTSUFFIX)] 

1199 self.setComment(name, value) 

1200 return 

1201 if isinstance(value, Mapping): 

1202 # Create a property set instead 

1203 ps = PropertySet() 

1204 for k, v in value.items(): 

1205 ps[k] = v 

1206 value = ps 

1207 self.set(name, value) 

1208 

1209 def __reduce__(self): 

1210 # It would be a bit simpler to use __setstate__ and __getstate__. 

1211 # However, implementing __setstate__ in Python causes segfaults 

1212 # because pickle creates a new instance by calling 

1213 # object.__new__(PropertyList, *args) which bypasses 

1214 # the pybind11 memory allocation step. 

1215 return (_makePropertyList, (getPropertyListState(self),)) 

1216 

1217 def get_dict(self, key: str) -> NestedMetadataDict: 

1218 """Return a possibly-hierarchical nested `dict`. 

1219 

1220 This implements the `lsst.pipe.base.GetDictMetadata` protocol for 

1221 consistency with `lsst.pipe.base.TaskMetadata` and `PropertySet`. 

1222 

1223 Parameters 

1224 ---------- 

1225 key : `str` 

1226 String key associated with the mapping. May not have a ``.`` 

1227 character. 

1228 

1229 Returns 

1230 ------- 

1231 value : `~collections.abc.Mapping` 

1232 Possibly-nested mapping, with `str` keys and values that are `int`, 

1233 `float`, `str`, `bool`, or another `dict` with the same key and 

1234 value types. Will be empty if ``key`` does not exist. 

1235 """ 

1236 result: NestedMetadataDict = {} 

1237 name: str 

1238 for name in self.getOrderedNames(): 

1239 levels = name.split(".") 

1240 if levels[0] == key: 

1241 nested = result 

1242 for level_key in levels[1:-1]: 

1243 nested = result.setdefault(level_key, {}) 

1244 nested[levels[-1]] = self[name] 

1245 return result 

1246 

1247 def set_dict(self, key: str, value: NestedMetadataDict) -> None: 

1248 """Assign a possibly-hierarchical nested `dict`. 

1249 

1250 This implements the `lsst.pipe.base.SetDictMetadata` protocol for 

1251 consistency with `lsst.pipe.base.TaskMetadata` and `PropertySet`. 

1252 

1253 Parameters 

1254 ---------- 

1255 key : `str` 

1256 String key associated with the mapping. May not have a ``.`` 

1257 character. 

1258 value : `~collections.abc.Mapping` 

1259 Possibly-nested mapping, with `str` keys and values that are `int`, 

1260 `float`, `str`, `bool`, or another `dict` with the same key and 

1261 value types. Nested keys may not have a ``.`` character. 

1262 """ 

1263 self.set(key, PropertySet.from_mapping(value))