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

338 statements  

« prev     ^ index     » next       coverage.py v7.3.1, created at 2023-09-19 10:14 +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 

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 .._dafBaseLib import PropertySet, PropertyList 

38from ..dateTime import DateTime 

39 

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

41_TYPE_MAP = {} 

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

43 "Float", "Double", "String", "DateTime", 

44 "PropertySet", "Undef"): 

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

46 _TYPE_MAP[type_obj] = checkType 

47 # Store both directions. 

48 _TYPE_MAP[checkType] = type_obj 

49 

50 

51def getPropertySetState(container, asLists=False): 

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

53 

54 Parameters 

55 ---------- 

56 container : `PropertySet` 

57 The property container. 

58 asLists : `bool`, optional 

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

60 a `list` will be used. 

61 

62 Returns 

63 ------- 

64 state : `list` of `tuple` or `list` of `list` 

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

66 the following 3 items: 

67 

68 name (a `str`) 

69 the name of the item 

70 elementTypeName (a `str`) 

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

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

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

74 the ``setInt`` method. 

75 value 

76 the data for the item, in a form compatible 

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

78 """ 

79 names = container.names(topLevelOnly=True) 

80 sequence = list if asLists else tuple 

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

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

83 for name in names] 

84 

85 

86def getPropertyListState(container, asLists=False): 

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

88 

89 Parameters 

90 ---------- 

91 container : `PropertyList` 

92 The property container. 

93 asLists : `bool`, optional 

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

95 a `list` will be used. 

96 

97 Returns 

98 ------- 

99 state : `list` of `tuple` or `list` of `list` 

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

101 the following 4 items: 

102 

103 name (a `str`): 

104 the name of the item 

105 elementTypeName (a `str`): 

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

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

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

109 the ``setInt`` method. 

110 value 

111 the data for the item, in a form compatible 

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

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

114 if ``container`` is a PropertyList. 

115 """ 

116 sequence = list if asLists else tuple 

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

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

119 container.getComment(name))) 

120 for name in container.getOrderedNames()] 

121 

122 

123def setPropertySetState(container, state): 

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

125 

126 Parameters 

127 ---------- 

128 container : `PropertySet` 

129 The property container whose state is to be restored. 

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

131 state : `list` 

132 The state, as returned by `getPropertySetState` 

133 """ 

134 for name, elemType, value in state: 

135 if elemType is not None: 

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

137 else: 

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

139 

140 

141def setPropertyListState(container, state): 

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

143 

144 Parameters 

145 ---------- 

146 container : `PropertyList` 

147 The property container whose state is to be restored. 

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

149 state : `list` 

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

151 """ 

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

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

154 

155 

156class ReturnStyle(enum.Enum): 

157 ARRAY = enum.auto() 

158 SCALAR = enum.auto() 

159 AUTO = enum.auto() 

160 

161 

162def _propertyContainerElementTypeName(container, name): 

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

164 

165 Parameters 

166 ---------- 

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

168 Container including the element 

169 name : `str` 

170 Name of element 

171 """ 

172 try: 

173 t = container.typeOf(name) 

174 except LookupError as e: 

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

176 # from a mapping. 

177 raise KeyError(str(e)) from None 

178 

179 return _TYPE_MAP.get(t, None) 

180 

181 

182def _propertyContainerGet(container, name, returnStyle): 

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

184 

185 Parameters 

186 ---------- 

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

188 Container from which to get the value 

189 name : `str` 

190 Name of item 

191 returnStyle : `ReturnStyle` 

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

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

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

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

196 as an array of values. 

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

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

199 return the last value. 

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

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

202 otherwise. 

203 

204 Raises 

205 ------ 

206 KeyError 

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

208 TypeError 

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

210 ValueError 

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

212 """ 

213 if not container.exists(name): 

214 raise KeyError(name + " not found") 

215 if returnStyle not in ReturnStyle: 

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

217 

218 elemType = _propertyContainerElementTypeName(container, name) 

219 if elemType and elemType != "PropertySet": 

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

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

222 return value 

223 return value[-1] 

224 

225 if container.isPropertySetPtr(name): 

226 try: 

227 return container.getAsPropertyListPtr(name) 

228 except Exception: 

229 return container.getAsPropertySetPtr(name) 

230 try: 

231 return container.getAsPersistablePtr(name) 

232 except Exception: 

233 pass 

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

235 

236 

237def _iterable(a): 

238 """Make input iterable. 

239 

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

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

242 yields itself. 

243 """ 

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

245 yield a 

246 return 

247 try: 

248 yield from a 

249 except Exception: 

250 yield a 

251 

252 

253def _guessIntegerType(container, name, value): 

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

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

256 is assumed to be a scalar. 

257 

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

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

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

261 

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

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

264 

265 Parameters 

266 ---------- 

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

268 Container from which to get the value 

269 

270 name : `str` 

271 Name of item 

272 

273 value : `object` 

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

275 

276 Returns 

277 ------- 

278 useType : `str` or none 

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

280 `bool` or a non-integral value. 

281 """ 

282 maxInt = 2147483647 

283 minInt = -2147483648 

284 maxLongLong = 2**63 - 1 

285 minLongLong = -2**63 

286 maxU64 = 2**64 - 1 

287 minU64 = 0 

288 

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

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

291 min = None 

292 max = None 

293 for v in _iterable(value): 

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

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

296 return None 

297 

298 if min is None: 

299 min = v 

300 max = v 

301 elif v < min: 

302 min = v 

303 elif v > max: 

304 max = v 

305 

306 # Safety net 

307 if min is None or max is None: 

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

309 

310 def _choose_int_from_range(int_value, current_type): 

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

312 # does not matter. 

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

314 current_type = None 

315 

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

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

318 # current type is an Int. 

319 use_type = "Int" 

320 elif int_value >= minLongLong and int_value < 0: 

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

322 # in Int clause above. 

323 use_type = "LongLong" 

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

325 # Larger than Int or already a LongLong 

326 use_type = "LongLong" 

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

328 use_type = "UnsignedLongLong" 

329 else: 

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

331 f"range value: {int_value}") 

332 return use_type 

333 

334 if container.exists(name): 

335 containerType = _propertyContainerElementTypeName(container, name) 

336 else: 

337 containerType = None 

338 

339 useTypeMin = _choose_int_from_range(min, containerType) 

340 useTypeMax = _choose_int_from_range(max, containerType) 

341 

342 if useTypeMin == useTypeMax: 

343 return useTypeMin 

344 

345 # When different the combinations are: 

346 # Int + LongLong 

347 # Int + UnsignedLongLong 

348 # LongLong + UnsignedLongLong 

349 

350 choices = {useTypeMin, useTypeMax} 

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

352 return "LongLong" 

353 

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

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

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

357 if "UnsignedLongLong" in choices: 

358 return "UnsignedLongLong" 

359 

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

361 

362 

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

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

365 """ 

366 try: 

367 exemplar = next(_iterable(value)) 

368 except StopIteration: 

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

370 # of the explicit setX() methods. 

371 return 

372 t = type(exemplar) 

373 setType = _guessIntegerType(container, name, value) 

374 

375 if setType is not None or t in typeMenu: 

376 if setType is None: 

377 setType = typeMenu[t] 

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

379 # Allow for subclasses 

380 for checkType in typeMenu: 

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

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

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

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

385 

386 

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

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

389 """ 

390 try: 

391 exemplar = next(_iterable(value)) 

392 except StopIteration: 

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

394 # since there is nothing to add. 

395 return 

396 t = type(exemplar) 

397 addType = _guessIntegerType(container, name, exemplar) 

398 

399 if addType is not None or t in typeMenu: 

400 if addType is None: 

401 addType = typeMenu[t] 

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

403 # Allow for subclasses 

404 for checkType in typeMenu: 

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

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

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

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

409 

410 

411def _makePropertySet(state): 

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

413 

414 Parameters 

415 ---------- 

416 state : `list` 

417 The data returned by `getPropertySetState`. 

418 """ 

419 ps = PropertySet() 

420 setPropertySetState(ps, state) 

421 return ps 

422 

423 

424def _makePropertyList(state): 

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

426 `getPropertyListState` 

427 

428 Parameters 

429 ---------- 

430 state : `list` 

431 The data returned by `getPropertySetState`. 

432 """ 

433 pl = PropertyList() 

434 setPropertyListState(pl, state) 

435 return pl 

436 

437 

438@continueClass 

439class PropertySet: 

440 # Mapping of type to method names; 

441 # int types are omitted due to use of _guessIntegerType 

442 _typeMenu = {bool: "Bool", 

443 float: "Double", 

444 str: "String", 

445 DateTime: "DateTime", 

446 PropertySet: "PropertySet", 

447 PropertyList: "PropertySet", 

448 None: "Undef", 

449 } 

450 

451 @classmethod 

452 def from_mapping(cls, metadata): 

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

454 

455 Parameters 

456 ---------- 

457 metadata : `collections.abc.Mapping` 

458 Metadata from which to create the `PropertySet`. 

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

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

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

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

463 

464 Returns 

465 ------- 

466 ps : `PropertySet` 

467 The new `PropertySet`. 

468 """ 

469 ps = cls() 

470 d = None 

471 if isinstance(metadata, Mapping): 

472 d = metadata 

473 elif dataclasses.is_dataclass(metadata): 

474 d = dataclasses.asdict(metadata) 

475 else: 

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

477 if hasattr(metadata, attr): 

478 d = getattr(metadata, attr)() 

479 break 

480 if d is None: 

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

482 f" {type(metadata)}") 

483 ps.update(d) 

484 return ps 

485 

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

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

488 

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

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

491 the final value in the array will be returned. 

492 

493 Parameters 

494 ---------- 

495 name : `str` 

496 Name of item 

497 default : `object`, optional 

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

499 

500 Returns 

501 ------- 

502 value : any type supported by container 

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

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

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

506 returned. 

507 """ 

508 try: 

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

510 except KeyError: 

511 return default 

512 

513 def getArray(self, name): 

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

515 

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

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

518 

519 Parameters 

520 ---------- 

521 name : `str` 

522 Name of item 

523 

524 Returns 

525 ------- 

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

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

528 

529 Raises 

530 ------ 

531 KeyError 

532 Raised if the item does not exist. 

533 """ 

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

535 

536 def getScalar(self, name): 

537 """Return an item as a scalar 

538 

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

540 

541 Parameters 

542 ---------- 

543 name : `str` 

544 Name of item 

545 

546 Returns 

547 ------- 

548 value : scalar item 

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

550 most recently added value is returned. 

551 

552 Raises 

553 ------ 

554 KeyError 

555 Raised if the item does not exist. 

556 """ 

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

558 

559 def set(self, name, value): 

560 """Set the value of an item 

561 

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

563 need not match. 

564 

565 Parameters 

566 ---------- 

567 name : `str` 

568 Name of item 

569 value : any supported type 

570 Value of item; may be a scalar or array 

571 """ 

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

573 

574 def add(self, name, value): 

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

576 

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

578 otherwise it is like calling `set` 

579 

580 Parameters 

581 ---------- 

582 name : `str` 

583 Name of item 

584 value : any supported type 

585 Value of item; may be a scalar or array 

586 

587 Notes 

588 ----- 

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

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

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

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

593 and vice-versa. 

594 

595 Raises 

596 ------ 

597 lsst::pex::exceptions::TypeError 

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

599 value of the item. 

600 """ 

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

602 

603 def update(self, addition): 

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

605 

606 Parameters 

607 ---------- 

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

609 The content to merge into the current container. 

610 

611 Notes 

612 ----- 

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

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

615 method updates by overwriting existing values completely with 

616 the new value. 

617 """ 

618 if isinstance(addition, PropertySet): 

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

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

621 for k in addition: 

622 self.copy(k, addition, k) 

623 else: 

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

625 self[k] = v 

626 

627 def toDict(self): 

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

629 

630 Returns 

631 ------- 

632 d : `dict` 

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

634 """ 

635 

636 d = {} 

637 for name in self.names(): 

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

639 

640 if isinstance(v, PropertySet): 

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

642 else: 

643 d[name] = v 

644 return d 

645 

646 def __eq__(self, other): 

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

648 return NotImplemented 

649 

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

651 return False 

652 

653 for name in self: 

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

655 return False 

656 

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

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

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

660 # equating two PropertySets if there are fields with NaN 

661 # these should equate equal. 

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

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

664 pass 

665 else: 

666 return False 

667 

668 return True 

669 

670 def __copy__(self): 

671 # Copy without having to go through pickle state 

672 ps = PropertySet() 

673 for itemName in self: 

674 ps.copy(itemName, self, itemName) 

675 return ps 

676 

677 def __deepcopy__(self, memo): 

678 result = self.deepCopy() 

679 memo[id(self)] = result 

680 return result 

681 

682 def __contains__(self, name): 

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

684 of the container. 

685 

686 Notes 

687 ------ 

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

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

690 items returned from ``__iter__``. 

691 """ 

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

693 

694 def __setitem__(self, name, value): 

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

696 

697 Parameters 

698 ---------- 

699 name : `str` 

700 Name of item to update. 

701 value : Value to assign 

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

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

704 `PropertySet` before assignment. 

705 

706 Notes 

707 ----- 

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

709 """ 

710 if isinstance(value, Mapping): 

711 # Create a property set instead 

712 ps = PropertySet() 

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

714 ps[k] = v 

715 value = ps 

716 self.set(name, value) 

717 

718 def __getitem__(self, name): 

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

720 

721 Notes 

722 ----- 

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

724 will be returned. 

725 """ 

726 return self.getScalar(name) 

727 

728 def __delitem__(self, name): 

729 if self.exists(name): 

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

731 self.remove(name) 

732 else: 

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

734 

735 def __str__(self): 

736 return self.toString() 

737 

738 def __len__(self): 

739 return self.nameCount(topLevelOnly=True) 

740 

741 def __iter__(self): 

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

743 yield n 

744 

745 def keys(self): 

746 return KeysView(self) 

747 

748 def items(self): 

749 return ItemsView(self) 

750 

751 def values(self): 

752 return ValuesView(self) 

753 

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

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

756 

757 Parameters 

758 ---------- 

759 name : `str` 

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

761 default : Any, optional 

762 Value to return if the key is not present. 

763 

764 Returns 

765 ------- 

766 value : Any 

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

768 

769 Raises 

770 ------ 

771 KeyError 

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

773 """ 

774 if self.exists(name): 

775 value = self[name] 

776 self.remove(name) 

777 else: 

778 if default is None: 

779 raise KeyError(name) 

780 value = default 

781 return value 

782 

783 def __reduce__(self): 

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

785 # However, implementing __setstate__ in Python causes segfaults 

786 # because pickle creates a new instance by calling 

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

788 # the pybind11 memory allocation step. 

789 return (_makePropertySet, (getPropertySetState(self),)) 

790 

791 

792@continueClass 

793class PropertyList: 

794 # Mapping of type to method names 

795 _typeMenu = {bool: "Bool", 

796 int: "Int", 

797 float: "Double", 

798 str: "String", 

799 DateTime: "DateTime", 

800 PropertySet: "PropertySet", 

801 PropertyList: "PropertySet", 

802 None: "Undef", 

803 } 

804 

805 COMMENTSUFFIX = "#COMMENT" 

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

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

808 

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

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

811 

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

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

814 the final value in the array will be returned. 

815 

816 Parameters 

817 ---------- 

818 name : ``str`` 

819 Name of item 

820 default : `object`, optional 

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

822 

823 Returns 

824 ------- 

825 value : any type supported by container 

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

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

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

829 returned. 

830 """ 

831 try: 

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

833 except KeyError: 

834 return default 

835 

836 def getArray(self, name): 

837 """Return an item as a list. 

838 

839 Parameters 

840 ---------- 

841 name : `str` 

842 Name of item 

843 

844 Returns 

845 ------- 

846 values : `list` of values 

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

848 

849 Raises 

850 ------ 

851 KeyError 

852 Raised if the item does not exist. 

853 """ 

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

855 

856 def getScalar(self, name): 

857 """Return an item as a scalar 

858 

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

860 

861 Parameters 

862 ---------- 

863 name : `str` 

864 Name of item. 

865 

866 Returns 

867 ------- 

868 value : scalar item 

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

870 most recently added value is returned. 

871 

872 Raises 

873 ------ 

874 KeyError 

875 Raised if the item does not exist. 

876 """ 

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

878 

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

880 """Set the value of an item 

881 

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

883 need not match. 

884 

885 Parameters 

886 ---------- 

887 name : `str` 

888 Name of item 

889 value : any supported type 

890 Value of item; may be a scalar or array 

891 """ 

892 args = [] 

893 if comment is not None: 

894 args.append(comment) 

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

896 

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

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

899 

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

901 otherwise it is like calling `set` 

902 

903 Parameters 

904 ---------- 

905 name : `str` 

906 Name of item 

907 value : any supported type 

908 Value of item; may be a scalar or array 

909 

910 Notes 

911 ----- 

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

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

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

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

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

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

918 

919 Raises 

920 ------ 

921 lsst::pex::exceptions::TypeError 

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

923 value of the item. 

924 """ 

925 args = [] 

926 if comment is not None: 

927 args.append(comment) 

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

929 

930 def setComment(self, name, comment): 

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

932 

933 Parameters 

934 ---------- 

935 name : `str` 

936 Name of the key to receive updated comment. 

937 comment : `comment` 

938 New comment string. 

939 """ 

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

941 # one that has the new comment 

942 containerType = _propertyContainerElementTypeName(self, name) 

943 if self.isArray(name): 

944 value = self.getArray(name) 

945 else: 

946 value = self.getScalar(name) 

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

948 

949 def toList(self): 

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

951 in the order that they were inserted. 

952 

953 Returns 

954 ------- 

955 ret : `list` of `tuple` 

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

957 in which they were inserted. 

958 """ 

959 orderedNames = self.getOrderedNames() 

960 ret = [] 

961 for name in orderedNames: 

962 if self.isArray(name): 

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

964 for v in values: 

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

966 else: 

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

968 self.getComment(name))) 

969 return ret 

970 

971 def toOrderedDict(self): 

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

973 they were inserted. 

974 

975 Returns 

976 ------- 

977 d : `dict` 

978 Ordered dictionary with all properties in the order that they 

979 were inserted. Comments are not included. 

980 

981 Notes 

982 ----- 

983 As of Python 3.6 dicts retain their insertion order. 

984 """ 

985 d = {} 

986 for name in self: 

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

988 return d 

989 

990 # For PropertyList the two are equivalent 

991 toDict = toOrderedDict 

992 

993 def __eq__(self, other): 

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

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

996 # doesn't either. 

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

998 return False 

999 

1000 for name in self: 

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

1002 return False 

1003 

1004 return True 

1005 

1006 def __copy__(self): 

1007 # Copy without having to go through pickle state 

1008 pl = PropertyList() 

1009 for itemName in self: 

1010 pl.copy(itemName, self, itemName) 

1011 return pl 

1012 

1013 def __deepcopy__(self, memo): 

1014 result = self.deepCopy() 

1015 memo[id(self)] = result 

1016 return result 

1017 

1018 def __iter__(self): 

1019 for n in self.getOrderedNames(): 

1020 yield n 

1021 

1022 def __setitem__(self, name, value): 

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

1024 

1025 Parameters 

1026 ---------- 

1027 name : `str` 

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

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

1030 than the value. 

1031 value : Value to assign 

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

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

1034 `PropertySet` before assignment. 

1035 

1036 Notes 

1037 ----- 

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

1039 """ 

1040 if name.endswith(self.COMMENTSUFFIX): 

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

1042 self.setComment(name, value) 

1043 return 

1044 if isinstance(value, Mapping): 

1045 # Create a property set instead 

1046 ps = PropertySet() 

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

1048 ps[k] = v 

1049 value = ps 

1050 self.set(name, value) 

1051 

1052 def __reduce__(self): 

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

1054 # However, implementing __setstate__ in Python causes segfaults 

1055 # because pickle creates a new instance by calling 

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

1057 # the pybind11 memory allocation step. 

1058 return (_makePropertyList, (getPropertyListState(self),))