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

358 statements  

« prev     ^ index     » next       coverage.py v7.4.4, created at 2024-04-06 03:46 -0700

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 dataclasses 

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

31from typing import TypeAlias, Union 

32 

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

34import lsst.pex.exceptions # noqa: F401 

35from lsst.utils import continueClass 

36 

37from .._dafBaseLib import PropertySet, PropertyList 

38from ..dateTime import DateTime 

39 

40 

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

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

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

44 

45 

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

47_TYPE_MAP = {} 

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

49 "Float", "Double", "String", "DateTime", 

50 "PropertySet", "Undef"): 

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

52 _TYPE_MAP[type_obj] = checkType 

53 # Store both directions. 

54 _TYPE_MAP[checkType] = type_obj 

55 

56 

57def getPropertySetState(container, asLists=False): 

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

59 

60 Parameters 

61 ---------- 

62 container : `PropertySet` 

63 The property container. 

64 asLists : `bool`, optional 

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

66 a `list` will be used. 

67 

68 Returns 

69 ------- 

70 state : `list` of `tuple` or `list` of `list` 

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

72 the following 3 items: 

73 

74 name (a `str`) 

75 the name of the item 

76 elementTypeName (a `str`) 

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

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

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

80 the ``setInt`` method. 

81 value 

82 the data for the item, in a form compatible 

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

84 """ 

85 names = container.names(topLevelOnly=True) 

86 sequence = list if asLists else tuple 

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

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

89 for name in names] 

90 

91 

92def getPropertyListState(container, asLists=False): 

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

94 

95 Parameters 

96 ---------- 

97 container : `PropertyList` 

98 The property container. 

99 asLists : `bool`, optional 

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

101 a `list` will be used. 

102 

103 Returns 

104 ------- 

105 state : `list` of `tuple` or `list` of `list` 

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

107 the following 4 items: 

108 

109 name (a `str`): 

110 the name of the item 

111 elementTypeName (a `str`): 

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

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

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

115 the ``setInt`` method. 

116 value 

117 the data for the item, in a form compatible 

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

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

120 if ``container`` is a PropertyList. 

121 """ 

122 sequence = list if asLists else tuple 

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

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

125 container.getComment(name))) 

126 for name in container.getOrderedNames()] 

127 

128 

129def setPropertySetState(container, state): 

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

131 

132 Parameters 

133 ---------- 

134 container : `PropertySet` 

135 The property container whose state is to be restored. 

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

137 state : `list` 

138 The state, as returned by `getPropertySetState` 

139 """ 

140 for name, elemType, value in state: 

141 if elemType is not None: 

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

143 else: 

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

145 

146 

147def setPropertyListState(container, state): 

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

149 

150 Parameters 

151 ---------- 

152 container : `PropertyList` 

153 The property container whose state is to be restored. 

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

155 state : `list` 

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

157 """ 

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

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

160 

161 

162class ReturnStyle(enum.Enum): 

163 ARRAY = enum.auto() 

164 SCALAR = enum.auto() 

165 AUTO = enum.auto() 

166 

167 

168def _propertyContainerElementTypeName(container, name): 

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

170 

171 Parameters 

172 ---------- 

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

174 Container including the element 

175 name : `str` 

176 Name of element 

177 """ 

178 try: 

179 t = container.typeOf(name) 

180 except LookupError as e: 

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

182 # from a mapping. 

183 raise KeyError(str(e)) from None 

184 

185 return _TYPE_MAP.get(t, None) 

186 

187 

188def _propertyContainerGet(container, name, returnStyle): 

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

190 

191 Parameters 

192 ---------- 

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

194 Container from which to get the value 

195 name : `str` 

196 Name of item 

197 returnStyle : `ReturnStyle` 

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

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

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

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

202 as an array of values. 

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

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

205 return the last value. 

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

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

208 otherwise. 

209 

210 Raises 

211 ------ 

212 KeyError 

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

214 TypeError 

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

216 ValueError 

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

218 """ 

219 if not container.exists(name): 

220 raise KeyError(name + " not found") 

221 if returnStyle not in ReturnStyle: 

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

223 

224 elemType = _propertyContainerElementTypeName(container, name) 

225 if elemType and elemType != "PropertySet": 

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

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

228 return value 

229 return value[-1] 

230 

231 if container.isPropertySetPtr(name): 

232 try: 

233 return container.getAsPropertyListPtr(name) 

234 except Exception: 

235 return container.getAsPropertySetPtr(name) 

236 try: 

237 return container.getAsPersistablePtr(name) 

238 except Exception: 

239 pass 

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

241 

242 

243def _iterable(a): 

244 """Make input iterable. 

245 

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

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

248 yields itself. 

249 """ 

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

251 yield a 

252 return 

253 try: 

254 yield from a 

255 except Exception: 

256 yield a 

257 

258 

259def _guessIntegerType(container, name, value): 

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

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

262 is assumed to be a scalar. 

263 

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

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

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

267 

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

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

270 

271 Parameters 

272 ---------- 

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

274 Container from which to get the value 

275 

276 name : `str` 

277 Name of item 

278 

279 value : `object` 

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

281 

282 Returns 

283 ------- 

284 useType : `str` or none 

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

286 `bool` or a non-integral value. 

287 """ 

288 maxInt = 2147483647 

289 minInt = -2147483648 

290 maxLongLong = 2**63 - 1 

291 minLongLong = -2**63 

292 maxU64 = 2**64 - 1 

293 minU64 = 0 

294 

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

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

297 min = None 

298 max = None 

299 for v in _iterable(value): 

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

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

302 return None 

303 

304 if min is None: 

305 min = v 

306 max = v 

307 elif v < min: 

308 min = v 

309 elif v > max: 

310 max = v 

311 

312 # Safety net 

313 if min is None or max is None: 

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

315 

316 def _choose_int_from_range(int_value, current_type): 

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

318 # does not matter. 

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

320 current_type = None 

321 

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

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

324 # current type is an Int. 

325 use_type = "Int" 

326 elif int_value >= minLongLong and int_value < 0: 

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

328 # in Int clause above. 

329 use_type = "LongLong" 

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

331 # Larger than Int or already a LongLong 

332 use_type = "LongLong" 

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

334 use_type = "UnsignedLongLong" 

335 else: 

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

337 f"range value: {int_value}") 

338 return use_type 

339 

340 if container.exists(name): 

341 containerType = _propertyContainerElementTypeName(container, name) 

342 else: 

343 containerType = None 

344 

345 useTypeMin = _choose_int_from_range(min, containerType) 

346 useTypeMax = _choose_int_from_range(max, containerType) 

347 

348 if useTypeMin == useTypeMax: 

349 return useTypeMin 

350 

351 # When different the combinations are: 

352 # Int + LongLong 

353 # Int + UnsignedLongLong 

354 # LongLong + UnsignedLongLong 

355 

356 choices = {useTypeMin, useTypeMax} 

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

358 return "LongLong" 

359 

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

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

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

363 if "UnsignedLongLong" in choices: 

364 return "UnsignedLongLong" 

365 

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

367 

368 

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

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

371 """ 

372 try: 

373 exemplar = next(_iterable(value)) 

374 except StopIteration: 

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

376 # of the explicit setX() methods. 

377 return 

378 t = type(exemplar) 

379 setType = _guessIntegerType(container, name, value) 

380 

381 if setType is not None or t in typeMenu: 

382 if setType is None: 

383 setType = typeMenu[t] 

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

385 # Allow for subclasses 

386 for checkType in typeMenu: 

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

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

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

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

391 

392 

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

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

395 """ 

396 try: 

397 exemplar = next(_iterable(value)) 

398 except StopIteration: 

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

400 # since there is nothing to add. 

401 return 

402 t = type(exemplar) 

403 addType = _guessIntegerType(container, name, exemplar) 

404 

405 if addType is not None or t in typeMenu: 

406 if addType is None: 

407 addType = typeMenu[t] 

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

409 # Allow for subclasses 

410 for checkType in typeMenu: 

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

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

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

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

415 

416 

417def _makePropertySet(state): 

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

419 

420 Parameters 

421 ---------- 

422 state : `list` 

423 The data returned by `getPropertySetState`. 

424 """ 

425 ps = PropertySet() 

426 setPropertySetState(ps, state) 

427 return ps 

428 

429 

430def _makePropertyList(state): 

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

432 `getPropertyListState` 

433 

434 Parameters 

435 ---------- 

436 state : `list` 

437 The data returned by `getPropertySetState`. 

438 """ 

439 pl = PropertyList() 

440 setPropertyListState(pl, state) 

441 return pl 

442 

443 

444@continueClass 

445class PropertySet: 

446 # Mapping of type to method names; 

447 # int types are omitted due to use of _guessIntegerType 

448 _typeMenu = {bool: "Bool", 

449 float: "Double", 

450 str: "String", 

451 DateTime: "DateTime", 

452 PropertySet: "PropertySet", 

453 PropertyList: "PropertySet", 

454 None: "Undef", 

455 } 

456 

457 @classmethod 

458 def from_mapping(cls, metadata): 

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

460 

461 Parameters 

462 ---------- 

463 metadata : `collections.abc.Mapping` 

464 Metadata from which to create the `PropertySet`. 

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

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

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

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

469 

470 Returns 

471 ------- 

472 ps : `PropertySet` 

473 The new `PropertySet`. 

474 """ 

475 ps = cls() 

476 d = None 

477 if isinstance(metadata, Mapping): 

478 d = metadata 

479 elif dataclasses.is_dataclass(metadata): 

480 d = dataclasses.asdict(metadata) 

481 else: 

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

483 if hasattr(metadata, attr): 

484 d = getattr(metadata, attr)() 

485 break 

486 if d is None: 

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

488 f" {type(metadata)}") 

489 ps.update(d) 

490 return ps 

491 

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

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

494 

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

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

497 the final value in the array will be returned. 

498 

499 Parameters 

500 ---------- 

501 name : `str` 

502 Name of item 

503 default : `object`, optional 

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

505 

506 Returns 

507 ------- 

508 value : any type supported by container 

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

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

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

512 returned. 

513 """ 

514 try: 

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

516 except KeyError: 

517 return default 

518 

519 def getArray(self, name): 

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

521 

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

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

524 

525 Parameters 

526 ---------- 

527 name : `str` 

528 Name of item 

529 

530 Returns 

531 ------- 

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

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

534 

535 Raises 

536 ------ 

537 KeyError 

538 Raised if the item does not exist. 

539 """ 

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

541 

542 def getScalar(self, name): 

543 """Return an item as a scalar 

544 

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

546 

547 Parameters 

548 ---------- 

549 name : `str` 

550 Name of item 

551 

552 Returns 

553 ------- 

554 value : scalar item 

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

556 most recently added value is returned. 

557 

558 Raises 

559 ------ 

560 KeyError 

561 Raised if the item does not exist. 

562 """ 

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

564 

565 def set(self, name, value): 

566 """Set the value of an item 

567 

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

569 need not match. 

570 

571 Parameters 

572 ---------- 

573 name : `str` 

574 Name of item 

575 value : any supported type 

576 Value of item; may be a scalar or array 

577 """ 

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

579 

580 def add(self, name, value): 

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

582 

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

584 otherwise it is like calling `set` 

585 

586 Parameters 

587 ---------- 

588 name : `str` 

589 Name of item 

590 value : any supported type 

591 Value of item; may be a scalar or array 

592 

593 Notes 

594 ----- 

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

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

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

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

599 and vice-versa. 

600 

601 Raises 

602 ------ 

603 lsst::pex::exceptions::TypeError 

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

605 value of the item. 

606 """ 

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

608 

609 def update(self, addition): 

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

611 

612 Parameters 

613 ---------- 

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

615 The content to merge into the current container. 

616 

617 Notes 

618 ----- 

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

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

621 method updates by overwriting existing values completely with 

622 the new value. 

623 """ 

624 if isinstance(addition, PropertySet): 

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

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

627 for k in addition: 

628 self.copy(k, addition, k) 

629 else: 

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

631 self[k] = v 

632 

633 def toDict(self): 

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

635 

636 Returns 

637 ------- 

638 d : `dict` 

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

640 """ 

641 

642 d = {} 

643 for name in self.names(): 

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

645 

646 if isinstance(v, PropertySet): 

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

648 else: 

649 d[name] = v 

650 return d 

651 

652 def __eq__(self, other): 

653 if type(self) != type(other): 

654 return NotImplemented 

655 

656 if len(self) != len(other): 

657 return False 

658 

659 for name in self: 

660 if (self_typeOf := self.typeOf(name)) != other.typeOf(name): 

661 return False 

662 

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

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

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

666 # equating two PropertySets if there are fields with NaN 

667 # these should equate equal. 

668 if self_typeOf in (_TYPE_MAP["Float"], _TYPE_MAP["Double"]) \ 

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

670 pass 

671 else: 

672 return False 

673 

674 return True 

675 

676 def __copy__(self): 

677 # Copy without having to go through pickle state 

678 ps = PropertySet() 

679 for itemName in self: 

680 ps.copy(itemName, self, itemName) 

681 return ps 

682 

683 def __deepcopy__(self, memo): 

684 result = self.deepCopy() 

685 memo[id(self)] = result 

686 return result 

687 

688 def __contains__(self, name): 

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

690 of the container. 

691 

692 Notes 

693 ------ 

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

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

696 items returned from ``__iter__``. 

697 """ 

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

699 

700 def __setitem__(self, name, value): 

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

702 

703 Parameters 

704 ---------- 

705 name : `str` 

706 Name of item to update. 

707 value : Value to assign 

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

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

710 `PropertySet` before assignment. 

711 

712 Notes 

713 ----- 

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

715 """ 

716 if isinstance(value, Mapping): 

717 # Create a property set instead 

718 ps = PropertySet() 

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

720 ps[k] = v 

721 value = ps 

722 self.set(name, value) 

723 

724 def __getitem__(self, name): 

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

726 

727 Notes 

728 ----- 

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

730 will be returned. 

731 """ 

732 return self.getScalar(name) 

733 

734 def __delitem__(self, name): 

735 if self.exists(name): 

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

737 self.remove(name) 

738 else: 

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

740 

741 def __str__(self): 

742 return self.toString() 

743 

744 def __len__(self): 

745 return self.nameCount(topLevelOnly=True) 

746 

747 def __iter__(self): 

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

749 yield n 

750 

751 def keys(self): 

752 return KeysView(self) 

753 

754 def items(self): 

755 return ItemsView(self) 

756 

757 def values(self): 

758 return ValuesView(self) 

759 

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

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

762 

763 Parameters 

764 ---------- 

765 name : `str` 

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

767 default : Any, optional 

768 Value to return if the key is not present. 

769 

770 Returns 

771 ------- 

772 value : Any 

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

774 

775 Raises 

776 ------ 

777 KeyError 

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

779 """ 

780 if self.exists(name): 

781 value = self[name] 

782 self.remove(name) 

783 else: 

784 if default is None: 

785 raise KeyError(name) 

786 value = default 

787 return value 

788 

789 def __reduce__(self): 

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

791 # However, implementing __setstate__ in Python causes segfaults 

792 # because pickle creates a new instance by calling 

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

794 # the pybind11 memory allocation step. 

795 return (_makePropertySet, (getPropertySetState(self),)) 

796 

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

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

799 

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

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

802 

803 Parameters 

804 ---------- 

805 key : `str` 

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

807 character. 

808 

809 Returns 

810 ------- 

811 value : `~collections.abc.Mapping` 

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

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

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

815 

816 Raises 

817 ------ 

818 TypeError 

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

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

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

822 """ 

823 if self.exists(key): 

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

825 else: 

826 return {} 

827 

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

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

830 

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

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

833 

834 Parameters 

835 ---------- 

836 key : `str` 

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

838 character. 

839 value : `~collections.abc.Mapping` 

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

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

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

843 character. 

844 """ 

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

846 

847 

848@continueClass 

849class PropertyList: 

850 # Mapping of type to method names 

851 _typeMenu = {bool: "Bool", 

852 int: "Int", 

853 float: "Double", 

854 str: "String", 

855 DateTime: "DateTime", 

856 PropertySet: "PropertySet", 

857 PropertyList: "PropertySet", 

858 None: "Undef", 

859 } 

860 

861 COMMENTSUFFIX = "#COMMENT" 

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

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

864 

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

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

867 

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

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

870 the final value in the array will be returned. 

871 

872 Parameters 

873 ---------- 

874 name : ``str`` 

875 Name of item 

876 default : `object`, optional 

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

878 

879 Returns 

880 ------- 

881 value : any type supported by container 

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

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

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

885 returned. 

886 """ 

887 try: 

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

889 except KeyError: 

890 return default 

891 

892 def getArray(self, name): 

893 """Return an item as a list. 

894 

895 Parameters 

896 ---------- 

897 name : `str` 

898 Name of item 

899 

900 Returns 

901 ------- 

902 values : `list` of values 

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

904 

905 Raises 

906 ------ 

907 KeyError 

908 Raised if the item does not exist. 

909 """ 

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

911 

912 def getScalar(self, name): 

913 """Return an item as a scalar 

914 

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

916 

917 Parameters 

918 ---------- 

919 name : `str` 

920 Name of item. 

921 

922 Returns 

923 ------- 

924 value : scalar item 

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

926 most recently added value is returned. 

927 

928 Raises 

929 ------ 

930 KeyError 

931 Raised if the item does not exist. 

932 """ 

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

934 

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

936 """Set the value of an item 

937 

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

939 need not match. 

940 

941 Parameters 

942 ---------- 

943 name : `str` 

944 Name of item 

945 value : any supported type 

946 Value of item; may be a scalar or array 

947 """ 

948 args = [] 

949 if comment is not None: 

950 args.append(comment) 

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

952 

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

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

955 

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

957 otherwise it is like calling `set` 

958 

959 Parameters 

960 ---------- 

961 name : `str` 

962 Name of item 

963 value : any supported type 

964 Value of item; may be a scalar or array 

965 

966 Notes 

967 ----- 

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

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

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

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

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

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

974 

975 Raises 

976 ------ 

977 lsst::pex::exceptions::TypeError 

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

979 value of the item. 

980 """ 

981 args = [] 

982 if comment is not None: 

983 args.append(comment) 

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

985 

986 def setComment(self, name, comment): 

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

988 

989 Parameters 

990 ---------- 

991 name : `str` 

992 Name of the key to receive updated comment. 

993 comment : `comment` 

994 New comment string. 

995 """ 

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

997 # one that has the new comment 

998 containerType = _propertyContainerElementTypeName(self, name) 

999 if self.isArray(name): 

1000 value = self.getArray(name) 

1001 else: 

1002 value = self.getScalar(name) 

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

1004 

1005 def toList(self): 

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

1007 in the order that they were inserted. 

1008 

1009 Returns 

1010 ------- 

1011 ret : `list` of `tuple` 

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

1013 in which they were inserted. 

1014 """ 

1015 orderedNames = self.getOrderedNames() 

1016 ret = [] 

1017 for name in orderedNames: 

1018 if self.isArray(name): 

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

1020 for v in values: 

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

1022 else: 

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

1024 self.getComment(name))) 

1025 return ret 

1026 

1027 def toOrderedDict(self): 

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

1029 they were inserted. 

1030 

1031 Returns 

1032 ------- 

1033 d : `dict` 

1034 Ordered dictionary with all properties in the order that they 

1035 were inserted. Comments are not included. 

1036 

1037 Notes 

1038 ----- 

1039 As of Python 3.6 dicts retain their insertion order. 

1040 """ 

1041 d = {} 

1042 for name in self: 

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

1044 return d 

1045 

1046 # For PropertyList the two are equivalent 

1047 toDict = toOrderedDict 

1048 

1049 def __eq__(self, other): 

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

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

1052 # doesn't either. 

1053 if not PropertySet.__eq__(self, other): 

1054 return False 

1055 

1056 for name in self: 

1057 if self.getComment(name) != other.getComment(name): 

1058 return False 

1059 

1060 return True 

1061 

1062 def __copy__(self): 

1063 # Copy without having to go through pickle state 

1064 pl = PropertyList() 

1065 for itemName in self: 

1066 pl.copy(itemName, self, itemName) 

1067 return pl 

1068 

1069 def __deepcopy__(self, memo): 

1070 result = self.deepCopy() 

1071 memo[id(self)] = result 

1072 return result 

1073 

1074 def __iter__(self): 

1075 for n in self.getOrderedNames(): 

1076 yield n 

1077 

1078 def __setitem__(self, name, value): 

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

1080 

1081 Parameters 

1082 ---------- 

1083 name : `str` 

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

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

1086 than the value. 

1087 value : Value to assign 

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

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

1090 `PropertySet` before assignment. 

1091 

1092 Notes 

1093 ----- 

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

1095 """ 

1096 if name.endswith(self.COMMENTSUFFIX): 

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

1098 self.setComment(name, value) 

1099 return 

1100 if isinstance(value, Mapping): 

1101 # Create a property set instead 

1102 ps = PropertySet() 

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

1104 ps[k] = v 

1105 value = ps 

1106 self.set(name, value) 

1107 

1108 def __reduce__(self): 

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

1110 # However, implementing __setstate__ in Python causes segfaults 

1111 # because pickle creates a new instance by calling 

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

1113 # the pybind11 memory allocation step. 

1114 return (_makePropertyList, (getPropertyListState(self),)) 

1115 

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

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

1118 

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

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

1121 

1122 Parameters 

1123 ---------- 

1124 key : `str` 

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

1126 character. 

1127 

1128 Returns 

1129 ------- 

1130 value : `~collections.abc.Mapping` 

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

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

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

1134 """ 

1135 result: NestedMetadataDict = {} 

1136 name: str 

1137 for name in self.getOrderedNames(): 

1138 levels = name.split(".") 

1139 if levels[0] == key: 

1140 nested = result 

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

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

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

1144 return result 

1145 

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

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

1148 

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

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

1151 

1152 Parameters 

1153 ---------- 

1154 key : `str` 

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

1156 character. 

1157 value : `~collections.abc.Mapping` 

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

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

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

1161 """ 

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