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

339 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2023-01-06 01:35 -0800

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 

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

26 

27import enum 

28import math 

29import numbers 

30import dataclasses 

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

32 

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

34import lsst.pex.exceptions # noqa: F401 

35from lsst.utils import continueClass 

36 

37from .propertySet import PropertySet 

38from .propertyList import PropertyList 

39from ..dateTime import DateTime 

40 

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

42_TYPE_MAP = {} 

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

44 "Float", "Double", "String", "DateTime", 

45 "PropertySet", "Undef"): 

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

47 _TYPE_MAP[type_obj] = checkType 

48 # Store both directions. 

49 _TYPE_MAP[checkType] = type_obj 

50 

51 

52def getPropertySetState(container, asLists=False): 

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

54 

55 Parameters 

56 ---------- 

57 container : `PropertySet` 

58 The property container. 

59 asLists : `bool`, optional 

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

61 a `list` will be used. 

62 

63 Returns 

64 ------- 

65 state : `list` of `tuple` or `list` of `list` 

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

67 the following 3 items: 

68 

69 name (a `str`) 

70 the name of the item 

71 elementTypeName (a `str`) 

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

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

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

75 the ``setInt`` method. 

76 value 

77 the data for the item, in a form compatible 

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

79 """ 

80 names = container.names(topLevelOnly=True) 

81 sequence = list if asLists else tuple 

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

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

84 for name in names] 

85 

86 

87def getPropertyListState(container, asLists=False): 

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

89 

90 Parameters 

91 ---------- 

92 container : `PropertyList` 

93 The property container. 

94 asLists : `bool`, optional 

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

96 a `list` will be used. 

97 

98 Returns 

99 ------- 

100 state : `list` of `tuple` or `list` of `list` 

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

102 the following 4 items: 

103 

104 name (a `str`): 

105 the name of the item 

106 elementTypeName (a `str`): 

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

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

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

110 the ``setInt`` method. 

111 value 

112 the data for the item, in a form compatible 

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

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

115 if ``container`` is a PropertyList. 

116 """ 

117 sequence = list if asLists else tuple 

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

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

120 container.getComment(name))) 

121 for name in container.getOrderedNames()] 

122 

123 

124def setPropertySetState(container, state): 

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

126 

127 Parameters 

128 ---------- 

129 container : `PropertySet` 

130 The property container whose state is to be restored. 

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

132 state : `list` 

133 The state, as returned by `getPropertySetState` 

134 """ 

135 for name, elemType, value in state: 

136 if elemType is not None: 

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

138 else: 

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

140 

141 

142def setPropertyListState(container, state): 

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

144 

145 Parameters 

146 ---------- 

147 container : `PropertyList` 

148 The property container whose state is to be restored. 

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

150 state : `list` 

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

152 """ 

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

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

155 

156 

157class ReturnStyle(enum.Enum): 

158 ARRAY = enum.auto() 

159 SCALAR = enum.auto() 

160 AUTO = enum.auto() 

161 

162 

163def _propertyContainerElementTypeName(container, name): 

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

165 

166 Parameters 

167 ---------- 

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

169 Container including the element 

170 name : `str` 

171 Name of element 

172 """ 

173 try: 

174 t = container.typeOf(name) 

175 except LookupError as e: 

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

177 # from a mapping. 

178 raise KeyError(str(e)) from None 

179 

180 return _TYPE_MAP.get(t, None) 

181 

182 

183def _propertyContainerGet(container, name, returnStyle): 

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

185 

186 Parameters 

187 ---------- 

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

189 Container from which to get the value 

190 name : `str` 

191 Name of item 

192 returnStyle : `ReturnStyle` 

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

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

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

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

197 as an array of values. 

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

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

200 return the last value. 

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

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

203 otherwise. 

204 

205 Raises 

206 ------ 

207 KeyError 

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

209 TypeError 

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

211 ValueError 

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

213 """ 

214 if not container.exists(name): 

215 raise KeyError(name + " not found") 

216 if returnStyle not in ReturnStyle: 

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

218 

219 elemType = _propertyContainerElementTypeName(container, name) 

220 if elemType and elemType != "PropertySet": 

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

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

223 return value 

224 return value[-1] 

225 

226 if container.isPropertySetPtr(name): 

227 try: 

228 return container.getAsPropertyListPtr(name) 

229 except Exception: 

230 return container.getAsPropertySetPtr(name) 

231 try: 

232 return container.getAsPersistablePtr(name) 

233 except Exception: 

234 pass 

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

236 

237 

238def _iterable(a): 

239 """Make input iterable. 

240 

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

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

243 yields itself. 

244 """ 

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

246 yield a 

247 return 

248 try: 

249 yield from a 

250 except Exception: 

251 yield a 

252 

253 

254def _guessIntegerType(container, name, value): 

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

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

257 is assumed to be a scalar. 

258 

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

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

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

262 

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

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

265 

266 Parameters 

267 ---------- 

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

269 Container from which to get the value 

270 

271 name : `str` 

272 Name of item 

273 

274 value : `object` 

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

276 

277 Returns 

278 ------- 

279 useType : `str` or none 

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

281 `bool` or a non-integral value. 

282 """ 

283 maxInt = 2147483647 

284 minInt = -2147483648 

285 maxLongLong = 2**63 - 1 

286 minLongLong = -2**63 

287 maxU64 = 2**64 - 1 

288 minU64 = 0 

289 

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

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

292 min = None 

293 max = None 

294 for v in _iterable(value): 

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

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

297 return None 

298 

299 if min is None: 

300 min = v 

301 max = v 

302 elif v < min: 

303 min = v 

304 elif v > max: 

305 max = v 

306 

307 # Safety net 

308 if min is None or max is None: 

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

310 

311 def _choose_int_from_range(int_value, current_type): 

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

313 # does not matter. 

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

315 current_type = None 

316 

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

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

319 # current type is an Int. 

320 use_type = "Int" 

321 elif int_value >= minLongLong and int_value < 0: 

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

323 # in Int clause above. 

324 use_type = "LongLong" 

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

326 # Larger than Int or already a LongLong 

327 use_type = "LongLong" 

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

329 use_type = "UnsignedLongLong" 

330 else: 

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

332 f"range value: {int_value}") 

333 return use_type 

334 

335 if container.exists(name): 

336 containerType = _propertyContainerElementTypeName(container, name) 

337 else: 

338 containerType = None 

339 

340 useTypeMin = _choose_int_from_range(min, containerType) 

341 useTypeMax = _choose_int_from_range(max, containerType) 

342 

343 if useTypeMin == useTypeMax: 

344 return useTypeMin 

345 

346 # When different the combinations are: 

347 # Int + LongLong 

348 # Int + UnsignedLongLong 

349 # LongLong + UnsignedLongLong 

350 

351 choices = {useTypeMin, useTypeMax} 

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

353 return "LongLong" 

354 

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

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

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

358 if "UnsignedLongLong" in choices: 

359 return "UnsignedLongLong" 

360 

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

362 

363 

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

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

366 """ 

367 try: 

368 exemplar = next(_iterable(value)) 

369 except StopIteration: 

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

371 # of the explicit setX() methods. 

372 return 

373 t = type(exemplar) 

374 setType = _guessIntegerType(container, name, value) 

375 

376 if setType is not None or t in typeMenu: 

377 if setType is None: 

378 setType = typeMenu[t] 

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

380 # Allow for subclasses 

381 for checkType in typeMenu: 

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

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

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

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

386 

387 

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

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

390 """ 

391 try: 

392 exemplar = next(_iterable(value)) 

393 except StopIteration: 

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

395 # since there is nothing to add. 

396 return 

397 t = type(exemplar) 

398 addType = _guessIntegerType(container, name, exemplar) 

399 

400 if addType is not None or t in typeMenu: 

401 if addType is None: 

402 addType = typeMenu[t] 

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

404 # Allow for subclasses 

405 for checkType in typeMenu: 

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

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

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

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

410 

411 

412def _makePropertySet(state): 

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

414 

415 Parameters 

416 ---------- 

417 state : `list` 

418 The data returned by `getPropertySetState`. 

419 """ 

420 ps = PropertySet() 

421 setPropertySetState(ps, state) 

422 return ps 

423 

424 

425def _makePropertyList(state): 

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

427 `getPropertyListState` 

428 

429 Parameters 

430 ---------- 

431 state : `list` 

432 The data returned by `getPropertySetState`. 

433 """ 

434 pl = PropertyList() 

435 setPropertyListState(pl, state) 

436 return pl 

437 

438 

439@continueClass 

440class PropertySet: 

441 # Mapping of type to method names; 

442 # int types are omitted due to use of _guessIntegerType 

443 _typeMenu = {bool: "Bool", 

444 float: "Double", 

445 str: "String", 

446 DateTime: "DateTime", 

447 PropertySet: "PropertySet", 

448 PropertyList: "PropertySet", 

449 None: "Undef", 

450 } 

451 

452 @classmethod 

453 def from_mapping(cls, metadata): 

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

455 

456 Parameters 

457 ---------- 

458 metadata : `collections.abc.Mapping` 

459 Metadata from which to create the `PropertySet`. 

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

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

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

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

464 

465 Returns 

466 ------- 

467 ps : `PropertySet` 

468 The new `PropertySet`. 

469 """ 

470 ps = cls() 

471 d = None 

472 if isinstance(metadata, Mapping): 

473 d = metadata 

474 elif dataclasses.is_dataclass(metadata): 

475 d = dataclasses.asdict(metadata) 

476 else: 

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

478 if hasattr(metadata, attr): 

479 d = getattr(metadata, attr)() 

480 break 

481 if d is None: 

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

483 f" {type(metadata)}") 

484 ps.update(d) 

485 return ps 

486 

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

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

489 

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

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

492 the final value in the array will be returned. 

493 

494 Parameters 

495 ---------- 

496 name : `str` 

497 Name of item 

498 default : `object`, optional 

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

500 

501 Returns 

502 ------- 

503 value : any type supported by container 

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

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

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

507 returned. 

508 """ 

509 try: 

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

511 except KeyError: 

512 return default 

513 

514 def getArray(self, name): 

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

516 

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

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

519 

520 Parameters 

521 ---------- 

522 name : `str` 

523 Name of item 

524 

525 Returns 

526 ------- 

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

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

529 

530 Raises 

531 ------ 

532 KeyError 

533 Raised if the item does not exist. 

534 """ 

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

536 

537 def getScalar(self, name): 

538 """Return an item as a scalar 

539 

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

541 

542 Parameters 

543 ---------- 

544 name : `str` 

545 Name of item 

546 

547 Returns 

548 ------- 

549 value : scalar item 

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

551 most recently added value is returned. 

552 

553 Raises 

554 ------ 

555 KeyError 

556 Raised if the item does not exist. 

557 """ 

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

559 

560 def set(self, name, value): 

561 """Set the value of an item 

562 

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

564 need not match. 

565 

566 Parameters 

567 ---------- 

568 name : `str` 

569 Name of item 

570 value : any supported type 

571 Value of item; may be a scalar or array 

572 """ 

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

574 

575 def add(self, name, value): 

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

577 

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

579 otherwise it is like calling `set` 

580 

581 Parameters 

582 ---------- 

583 name : `str` 

584 Name of item 

585 value : any supported type 

586 Value of item; may be a scalar or array 

587 

588 Notes 

589 ----- 

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

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

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

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

594 and vice-versa. 

595 

596 Raises 

597 ------ 

598 lsst::pex::exceptions::TypeError 

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

600 value of the item. 

601 """ 

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

603 

604 def update(self, addition): 

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

606 

607 Parameters 

608 ---------- 

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

610 The content to merge into the current container. 

611 

612 Notes 

613 ----- 

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

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

616 method updates by overwriting existing values completely with 

617 the new value. 

618 """ 

619 if isinstance(addition, PropertySet): 

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

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

622 for k in addition: 

623 self.copy(k, addition, k) 

624 else: 

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

626 self[k] = v 

627 

628 def toDict(self): 

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

630 

631 Returns 

632 ------- 

633 d : `dict` 

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

635 """ 

636 

637 d = {} 

638 for name in self.names(): 

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

640 

641 if isinstance(v, PropertySet): 

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

643 else: 

644 d[name] = v 

645 return d 

646 

647 def __eq__(self, other): 

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

649 return NotImplemented 

650 

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

652 return False 

653 

654 for name in self: 

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

656 return False 

657 

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

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

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

661 # equating two PropertySets if there are fields with NaN 

662 # these should equate equal. 

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

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

665 pass 

666 else: 

667 return False 

668 

669 return True 

670 

671 def __copy__(self): 

672 # Copy without having to go through pickle state 

673 ps = PropertySet() 

674 for itemName in self: 

675 ps.copy(itemName, self, itemName) 

676 return ps 

677 

678 def __deepcopy__(self, memo): 

679 result = self.deepCopy() 

680 memo[id(self)] = result 

681 return result 

682 

683 def __contains__(self, name): 

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

685 of the container. 

686 

687 Notes 

688 ------ 

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

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

691 items returned from ``__iter__``. 

692 """ 

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

694 

695 def __setitem__(self, name, value): 

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

697 

698 Parameters 

699 ---------- 

700 name : `str` 

701 Name of item to update. 

702 value : Value to assign 

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

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

705 `PropertySet` before assignment. 

706 

707 Notes 

708 ----- 

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

710 """ 

711 if isinstance(value, Mapping): 

712 # Create a property set instead 

713 ps = PropertySet() 

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

715 ps[k] = v 

716 value = ps 

717 self.set(name, value) 

718 

719 def __getitem__(self, name): 

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

721 

722 Notes 

723 ----- 

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

725 will be returned. 

726 """ 

727 return self.getScalar(name) 

728 

729 def __delitem__(self, name): 

730 if self.exists(name): 

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

732 self.remove(name) 

733 else: 

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

735 

736 def __str__(self): 

737 return self.toString() 

738 

739 def __len__(self): 

740 return self.nameCount(topLevelOnly=True) 

741 

742 def __iter__(self): 

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

744 yield n 

745 

746 def keys(self): 

747 return KeysView(self) 

748 

749 def items(self): 

750 return ItemsView(self) 

751 

752 def values(self): 

753 return ValuesView(self) 

754 

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

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

757 

758 Parameters 

759 ---------- 

760 name : `str` 

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

762 default : Any, optional 

763 Value to return if the key is not present. 

764 

765 Returns 

766 ------- 

767 value : Any 

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

769 

770 Raises 

771 ------ 

772 KeyError 

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

774 """ 

775 if self.exists(name): 

776 value = self[name] 

777 self.remove(name) 

778 else: 

779 if default is None: 

780 raise KeyError(name) 

781 value = default 

782 return value 

783 

784 def __reduce__(self): 

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

786 # However, implementing __setstate__ in Python causes segfaults 

787 # because pickle creates a new instance by calling 

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

789 # the pybind11 memory allocation step. 

790 return (_makePropertySet, (getPropertySetState(self),)) 

791 

792 

793@continueClass 

794class PropertyList: 

795 # Mapping of type to method names 

796 _typeMenu = {bool: "Bool", 

797 int: "Int", 

798 float: "Double", 

799 str: "String", 

800 DateTime: "DateTime", 

801 PropertySet: "PropertySet", 

802 PropertyList: "PropertySet", 

803 None: "Undef", 

804 } 

805 

806 COMMENTSUFFIX = "#COMMENT" 

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

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

809 

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

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

812 

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

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

815 the final value in the array will be returned. 

816 

817 Parameters 

818 ---------- 

819 name : ``str`` 

820 Name of item 

821 default : `object`, optional 

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

823 

824 Returns 

825 ------- 

826 value : any type supported by container 

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

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

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

830 returned. 

831 """ 

832 try: 

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

834 except KeyError: 

835 return default 

836 

837 def getArray(self, name): 

838 """Return an item as a list. 

839 

840 Parameters 

841 ---------- 

842 name : `str` 

843 Name of item 

844 

845 Returns 

846 ------- 

847 values : `list` of values 

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

849 

850 Raises 

851 ------ 

852 KeyError 

853 Raised if the item does not exist. 

854 """ 

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

856 

857 def getScalar(self, name): 

858 """Return an item as a scalar 

859 

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

861 

862 Parameters 

863 ---------- 

864 name : `str` 

865 Name of item. 

866 

867 Returns 

868 ------- 

869 value : scalar item 

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

871 most recently added value is returned. 

872 

873 Raises 

874 ------ 

875 KeyError 

876 Raised if the item does not exist. 

877 """ 

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

879 

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

881 """Set the value of an item 

882 

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

884 need not match. 

885 

886 Parameters 

887 ---------- 

888 name : `str` 

889 Name of item 

890 value : any supported type 

891 Value of item; may be a scalar or array 

892 """ 

893 args = [] 

894 if comment is not None: 

895 args.append(comment) 

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

897 

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

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

900 

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

902 otherwise it is like calling `set` 

903 

904 Parameters 

905 ---------- 

906 name : `str` 

907 Name of item 

908 value : any supported type 

909 Value of item; may be a scalar or array 

910 

911 Notes 

912 ----- 

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

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

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

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

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

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

919 

920 Raises 

921 ------ 

922 lsst::pex::exceptions::TypeError 

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

924 value of the item. 

925 """ 

926 args = [] 

927 if comment is not None: 

928 args.append(comment) 

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

930 

931 def setComment(self, name, comment): 

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

933 

934 Parameters 

935 ---------- 

936 name : `str` 

937 Name of the key to receive updated comment. 

938 comment : `comment` 

939 New comment string. 

940 """ 

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

942 # one that has the new comment 

943 containerType = _propertyContainerElementTypeName(self, name) 

944 if self.isArray(name): 

945 value = self.getArray(name) 

946 else: 

947 value = self.getScalar(name) 

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

949 

950 def toList(self): 

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

952 in the order that they were inserted. 

953 

954 Returns 

955 ------- 

956 ret : `list` of `tuple` 

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

958 in which they were inserted. 

959 """ 

960 orderedNames = self.getOrderedNames() 

961 ret = [] 

962 for name in orderedNames: 

963 if self.isArray(name): 

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

965 for v in values: 

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

967 else: 

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

969 self.getComment(name))) 

970 return ret 

971 

972 def toOrderedDict(self): 

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

974 they were inserted. 

975 

976 Returns 

977 ------- 

978 d : `dict` 

979 Ordered dictionary with all properties in the order that they 

980 were inserted. Comments are not included. 

981 

982 Notes 

983 ----- 

984 As of Python 3.6 dicts retain their insertion order. 

985 """ 

986 d = {} 

987 for name in self: 

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

989 return d 

990 

991 # For PropertyList the two are equivalent 

992 toDict = toOrderedDict 

993 

994 def __eq__(self, other): 

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

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

997 # doesn't either. 

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

999 return False 

1000 

1001 for name in self: 

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

1003 return False 

1004 

1005 return True 

1006 

1007 def __copy__(self): 

1008 # Copy without having to go through pickle state 

1009 pl = PropertyList() 

1010 for itemName in self: 

1011 pl.copy(itemName, self, itemName) 

1012 return pl 

1013 

1014 def __deepcopy__(self, memo): 

1015 result = self.deepCopy() 

1016 memo[id(self)] = result 

1017 return result 

1018 

1019 def __iter__(self): 

1020 for n in self.getOrderedNames(): 

1021 yield n 

1022 

1023 def __setitem__(self, name, value): 

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

1025 

1026 Parameters 

1027 ---------- 

1028 name : `str` 

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

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

1031 than the value. 

1032 value : Value to assign 

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

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

1035 `PropertySet` before assignment. 

1036 

1037 Notes 

1038 ----- 

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

1040 """ 

1041 if name.endswith(self.COMMENTSUFFIX): 

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

1043 self.setComment(name, value) 

1044 return 

1045 if isinstance(value, Mapping): 

1046 # Create a property set instead 

1047 ps = PropertySet() 

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

1049 ps[k] = v 

1050 value = ps 

1051 self.set(name, value) 

1052 

1053 def __reduce__(self): 

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

1055 # However, implementing __setstate__ in Python causes segfaults 

1056 # because pickle creates a new instance by calling 

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

1058 # the pybind11 memory allocation step. 

1059 return (_makePropertyList, (getPropertyListState(self),))