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

Shortcuts on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

334 statements  

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 numbers 

29import dataclasses 

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

31 

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

33import lsst.pex.exceptions # noqa: F401 

34from lsst.utils import continueClass 

35 

36from .propertySet import PropertySet 

37from .propertyList import PropertyList 

38from ..dateTime import DateTime 

39 

40 

41def getPropertySetState(container, asLists=False): 

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

43 

44 Parameters 

45 ---------- 

46 container : `PropertySet` 

47 The property container. 

48 asLists : `bool`, optional 

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

50 a `list` will be used. 

51 

52 Returns 

53 ------- 

54 state : `list` of `tuple` or `list` of `list` 

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

56 the following 3 items: 

57 

58 name (a `str`) 

59 the name of the item 

60 elementTypeName (a `str`) 

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

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

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

64 the ``setInt`` method. 

65 value 

66 the data for the item, in a form compatible 

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

68 """ 

69 names = container.names(topLevelOnly=True) 

70 sequence = list if asLists else tuple 

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

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

73 for name in names] 

74 

75 

76def getPropertyListState(container, asLists=False): 

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

78 

79 Parameters 

80 ---------- 

81 container : `PropertyList` 

82 The property container. 

83 asLists : `bool`, optional 

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

85 a `list` will be used. 

86 

87 Returns 

88 ------- 

89 state : `list` of `tuple` or `list` of `list` 

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

91 the following 4 items: 

92 

93 name (a `str`): 

94 the name of the item 

95 elementTypeName (a `str`): 

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

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

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

99 the ``setInt`` method. 

100 value 

101 the data for the item, in a form compatible 

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

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

104 if ``container`` is a PropertyList. 

105 """ 

106 sequence = list if asLists else tuple 

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

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

109 container.getComment(name))) 

110 for name in container.getOrderedNames()] 

111 

112 

113def setPropertySetState(container, state): 

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

115 

116 Parameters 

117 ---------- 

118 container : `PropertySet` 

119 The property container whose state is to be restored. 

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

121 state : `list` 

122 The state, as returned by `getPropertySetState` 

123 """ 

124 for name, elemType, value in state: 

125 if elemType is not None: 

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

127 else: 

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

129 

130 

131def setPropertyListState(container, state): 

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

133 

134 Parameters 

135 ---------- 

136 container : `PropertyList` 

137 The property container whose state is to be restored. 

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

139 state : `list` 

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

141 """ 

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

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

144 

145 

146class ReturnStyle(enum.Enum): 

147 ARRAY = enum.auto() 

148 SCALAR = enum.auto() 

149 AUTO = enum.auto() 

150 

151 

152def _propertyContainerElementTypeName(container, name): 

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

154 

155 Parameters 

156 ---------- 

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

158 Container including the element 

159 name : `str` 

160 Name of element 

161 """ 

162 try: 

163 t = container.typeOf(name) 

164 except LookupError as e: 

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

166 # from a mapping. 

167 raise KeyError(str(e)) 

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

169 "Float", "Double", "String", "DateTime", 

170 "PropertySet", "Undef"): 

171 if t == getattr(container, "TYPE_" + checkType): 

172 return checkType 

173 return None 

174 

175 

176def _propertyContainerGet(container, name, returnStyle): 

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

178 

179 Parameters 

180 ---------- 

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

182 Container from which to get the value 

183 name : `str` 

184 Name of item 

185 returnStyle : `ReturnStyle` 

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

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

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

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

190 as an array of values. 

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

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

193 return the last value. 

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

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

196 otherwise. 

197 

198 Raises 

199 ------ 

200 KeyError 

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

202 TypeError 

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

204 ValueError 

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

206 """ 

207 if not container.exists(name): 

208 raise KeyError(name + " not found") 

209 if returnStyle not in ReturnStyle: 

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

211 

212 elemType = _propertyContainerElementTypeName(container, name) 

213 if elemType and elemType != "PropertySet": 

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

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

216 return value 

217 return value[-1] 

218 

219 if container.isPropertySetPtr(name): 

220 try: 

221 return container.getAsPropertyListPtr(name) 

222 except Exception: 

223 return container.getAsPropertySetPtr(name) 

224 try: 

225 return container.getAsPersistablePtr(name) 

226 except Exception: 

227 pass 

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

229 

230 

231def _iterable(a): 

232 """Make input iterable. 

233 

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

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

236 yields itself. 

237 """ 

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

239 yield a 

240 return 

241 try: 

242 yield from a 

243 except Exception: 

244 yield a 

245 

246 

247def _guessIntegerType(container, name, value): 

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

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

250 is assumed to be a scalar. 

251 

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

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

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

255 

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

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

258 

259 Parameters 

260 ---------- 

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

262 Container from which to get the value 

263 

264 name : `str` 

265 Name of item 

266 

267 value : `object` 

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

269 

270 Returns 

271 ------- 

272 useType : `str` or none 

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

274 `bool` or a non-integral value. 

275 """ 

276 maxInt = 2147483647 

277 minInt = -2147483648 

278 maxLongLong = 2**63 - 1 

279 minLongLong = -2**63 

280 maxU64 = 2**64 - 1 

281 minU64 = 0 

282 

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

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

285 min = None 

286 max = None 

287 for v in _iterable(value): 

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

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

290 return None 

291 

292 if min is None: 

293 min = v 

294 max = v 

295 elif v < min: 

296 min = v 

297 elif v > max: 

298 max = v 

299 

300 # Safety net 

301 if min is None or max is None: 

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

303 

304 def _choose_int_from_range(int_value, current_type): 

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

306 # does not matter. 

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

308 current_type = None 

309 

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

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

312 # current type is an Int. 

313 use_type = "Int" 

314 elif int_value >= minLongLong and int_value < 0: 

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

316 # in Int clause above. 

317 use_type = "LongLong" 

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

319 # Larger than Int or already a LongLong 

320 use_type = "LongLong" 

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

322 use_type = "UnsignedLongLong" 

323 else: 

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

325 f"range value: {int_value}") 

326 return use_type 

327 

328 try: 

329 containerType = _propertyContainerElementTypeName(container, name) 

330 except LookupError: 

331 containerType = None 

332 

333 useTypeMin = _choose_int_from_range(min, containerType) 

334 useTypeMax = _choose_int_from_range(max, containerType) 

335 

336 if useTypeMin == useTypeMax: 

337 return useTypeMin 

338 

339 # When different the combinations are: 

340 # Int + LongLong 

341 # Int + UnsignedLongLong 

342 # LongLong + UnsignedLongLong 

343 

344 choices = {useTypeMin, useTypeMax} 

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

346 return "LongLong" 

347 

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

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

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

351 if "UnsignedLongLong" in choices: 

352 return "UnsignedLongLong" 

353 

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

355 

356 

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

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

359 """ 

360 try: 

361 exemplar = next(_iterable(value)) 

362 except StopIteration: 

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

364 # of the explicit setX() methods. 

365 return 

366 t = type(exemplar) 

367 setType = _guessIntegerType(container, name, value) 

368 

369 if setType is not None or t in typeMenu: 

370 if setType is None: 

371 setType = typeMenu[t] 

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

373 # Allow for subclasses 

374 for checkType in typeMenu: 

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

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

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

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

379 

380 

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

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

383 """ 

384 try: 

385 exemplar = next(_iterable(value)) 

386 except StopIteration: 

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

388 # since there is nothing to add. 

389 return 

390 t = type(exemplar) 

391 addType = _guessIntegerType(container, name, exemplar) 

392 

393 if addType is not None or t in typeMenu: 

394 if addType is None: 

395 addType = typeMenu[t] 

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

397 # Allow for subclasses 

398 for checkType in typeMenu: 

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

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

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

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

403 

404 

405def _makePropertySet(state): 

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

407 

408 Parameters 

409 ---------- 

410 state : `list` 

411 The data returned by `getPropertySetState`. 

412 """ 

413 ps = PropertySet() 

414 setPropertySetState(ps, state) 

415 return ps 

416 

417 

418def _makePropertyList(state): 

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

420 `getPropertyListState` 

421 

422 Parameters 

423 ---------- 

424 state : `list` 

425 The data returned by `getPropertySetState`. 

426 """ 

427 pl = PropertyList() 

428 setPropertyListState(pl, state) 

429 return pl 

430 

431 

432@continueClass 

433class PropertySet: 

434 # Mapping of type to method names; 

435 # int types are omitted due to use of _guessIntegerType 

436 _typeMenu = {bool: "Bool", 

437 float: "Double", 

438 str: "String", 

439 DateTime: "DateTime", 

440 PropertySet: "PropertySet", 

441 PropertyList: "PropertySet", 

442 None: "Undef", 

443 } 

444 

445 @classmethod 

446 def from_mapping(cls, metadata): 

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

448 

449 Parameters 

450 ---------- 

451 metadata : `collections.abc.Mapping` 

452 Metadata from which to create the `PropertySet`. 

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

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

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

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

457 

458 Returns 

459 ------- 

460 ps : `PropertySet` 

461 The new `PropertySet`. 

462 """ 

463 ps = cls() 

464 d = None 

465 if isinstance(metadata, Mapping): 

466 d = metadata 

467 elif dataclasses.is_dataclass(metadata): 

468 d = dataclasses.asdict(metadata) 

469 else: 

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

471 if hasattr(metadata, attr): 

472 d = getattr(metadata, attr)() 

473 break 

474 if d is None: 

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

476 f" {type(metadata)}") 

477 ps.update(d) 

478 return ps 

479 

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

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

482 

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

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

485 the final value in the array will be returned. 

486 

487 Parameters 

488 ---------- 

489 name : `str` 

490 Name of item 

491 default : `object`, optional 

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

493 

494 Returns 

495 ------- 

496 value : any type supported by container 

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

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

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

500 returned. 

501 """ 

502 try: 

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

504 except KeyError: 

505 return default 

506 

507 def getArray(self, name): 

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

509 

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

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

512 

513 Parameters 

514 ---------- 

515 name : `str` 

516 Name of item 

517 

518 Returns 

519 ------- 

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

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

522 

523 Raises 

524 ------ 

525 KeyError 

526 Raised if the item does not exist. 

527 """ 

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

529 

530 def getScalar(self, name): 

531 """Return an item as a scalar 

532 

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

534 

535 Parameters 

536 ---------- 

537 name : `str` 

538 Name of item 

539 

540 Returns 

541 ------- 

542 value : scalar item 

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

544 most recently added value is returned. 

545 

546 Raises 

547 ------ 

548 KeyError 

549 Raised if the item does not exist. 

550 """ 

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

552 

553 def set(self, name, value): 

554 """Set the value of an item 

555 

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

557 need not match. 

558 

559 Parameters 

560 ---------- 

561 name : `str` 

562 Name of item 

563 value : any supported type 

564 Value of item; may be a scalar or array 

565 """ 

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

567 

568 def add(self, name, value): 

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

570 

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

572 otherwise it is like calling `set` 

573 

574 Parameters 

575 ---------- 

576 name : `str` 

577 Name of item 

578 value : any supported type 

579 Value of item; may be a scalar or array 

580 

581 Notes 

582 ----- 

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

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

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

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

587 and vice-versa. 

588 

589 Raises 

590 ------ 

591 lsst::pex::exceptions::TypeError 

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

593 value of the item. 

594 """ 

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

596 

597 def update(self, addition): 

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

599 

600 Parameters 

601 ---------- 

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

603 The content to merge into the current container. 

604 

605 Notes 

606 ----- 

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

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

609 method updates by overwriting existing values completely with 

610 the new value. 

611 """ 

612 if isinstance(addition, PropertySet): 

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

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

615 for k in addition: 

616 self.copy(k, addition, k) 

617 else: 

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

619 self[k] = v 

620 

621 def toDict(self): 

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

623 

624 Returns 

625 ------- 

626 d : `dict` 

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

628 """ 

629 

630 d = {} 

631 for name in self.names(): 

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

633 

634 if isinstance(v, PropertySet): 

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

636 else: 

637 d[name] = v 

638 return d 

639 

640 def __eq__(self, other): 

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

642 return NotImplemented 

643 

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

645 return False 

646 

647 for name in self: 

648 if _propertyContainerGet(self, name, returnStyle=ReturnStyle.AUTO) != \ 

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

650 return False 

651 if self.typeOf(name) != other.typeOf(name): 

652 return False 

653 

654 return True 

655 

656 def __copy__(self): 

657 # Copy without having to go through pickle state 

658 ps = PropertySet() 

659 for itemName in self: 

660 ps.copy(itemName, self, itemName) 

661 return ps 

662 

663 def __deepcopy__(self, memo): 

664 result = self.deepCopy() 

665 memo[id(self)] = result 

666 return result 

667 

668 def __contains__(self, name): 

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

670 of the container. 

671 

672 Notes 

673 ------ 

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

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

676 items returned from ``__iter__``. 

677 """ 

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

679 

680 def __setitem__(self, name, value): 

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

682 

683 Parameters 

684 ---------- 

685 name : `str` 

686 Name of item to update. 

687 value : Value to assign 

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

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

690 `PropertySet` before assignment. 

691 

692 Notes 

693 ----- 

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

695 """ 

696 if isinstance(value, Mapping): 

697 # Create a property set instead 

698 ps = PropertySet() 

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

700 ps[k] = v 

701 value = ps 

702 self.set(name, value) 

703 

704 def __getitem__(self, name): 

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

706 

707 Notes 

708 ----- 

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

710 will be returned. 

711 """ 

712 return self.getScalar(name) 

713 

714 def __delitem__(self, name): 

715 if self.exists(name): 

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

717 self.remove(name) 

718 else: 

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

720 

721 def __str__(self): 

722 return self.toString() 

723 

724 def __len__(self): 

725 return self.nameCount(topLevelOnly=True) 

726 

727 def __iter__(self): 

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

729 yield n 

730 

731 def keys(self): 

732 return KeysView(self) 

733 

734 def items(self): 

735 return ItemsView(self) 

736 

737 def values(self): 

738 return ValuesView(self) 

739 

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

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

742 

743 Parameters 

744 ---------- 

745 name : `str` 

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

747 default : Any, optional 

748 Value to return if the key is not present. 

749 

750 Returns 

751 ------- 

752 value : Any 

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

754 

755 Raises 

756 ------ 

757 KeyError 

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

759 """ 

760 if self.exists(name): 

761 value = self[name] 

762 self.remove(name) 

763 else: 

764 if default is None: 

765 raise KeyError(name) 

766 value = default 

767 return value 

768 

769 def __reduce__(self): 

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

771 # However, implementing __setstate__ in Python causes segfaults 

772 # because pickle creates a new instance by calling 

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

774 # the pybind11 memory allocation step. 

775 return (_makePropertySet, (getPropertySetState(self),)) 

776 

777 

778@continueClass 

779class PropertyList: 

780 # Mapping of type to method names 

781 _typeMenu = {bool: "Bool", 

782 int: "Int", 

783 float: "Double", 

784 str: "String", 

785 DateTime: "DateTime", 

786 PropertySet: "PropertySet", 

787 PropertyList: "PropertySet", 

788 None: "Undef", 

789 } 

790 

791 COMMENTSUFFIX = "#COMMENT" 

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

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

794 

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

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

797 

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

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

800 the final value in the array will be returned. 

801 

802 Parameters 

803 ---------- 

804 name : ``str`` 

805 Name of item 

806 default : `object`, optional 

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

808 

809 Returns 

810 ------- 

811 value : any type supported by container 

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

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

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

815 returned. 

816 """ 

817 try: 

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

819 except KeyError: 

820 return default 

821 

822 def getArray(self, name): 

823 """Return an item as a list. 

824 

825 Parameters 

826 ---------- 

827 name : `str` 

828 Name of item 

829 

830 Returns 

831 ------- 

832 values : `list` of values 

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

834 

835 Raises 

836 ------ 

837 KeyError 

838 Raised if the item does not exist. 

839 """ 

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

841 

842 def getScalar(self, name): 

843 """Return an item as a scalar 

844 

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

846 

847 Parameters 

848 ---------- 

849 name : `str` 

850 Name of item. 

851 

852 Returns 

853 ------- 

854 value : scalar item 

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

856 most recently added value is returned. 

857 

858 Raises 

859 ------ 

860 KeyError 

861 Raised if the item does not exist. 

862 """ 

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

864 

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

866 """Set the value of an item 

867 

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

869 need not match. 

870 

871 Parameters 

872 ---------- 

873 name : `str` 

874 Name of item 

875 value : any supported type 

876 Value of item; may be a scalar or array 

877 """ 

878 args = [] 

879 if comment is not None: 

880 args.append(comment) 

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

882 

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

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

885 

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

887 otherwise it is like calling `set` 

888 

889 Parameters 

890 ---------- 

891 name : `str` 

892 Name of item 

893 value : any supported type 

894 Value of item; may be a scalar or array 

895 

896 Notes 

897 ----- 

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

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

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

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

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

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

904 

905 Raises 

906 ------ 

907 lsst::pex::exceptions::TypeError 

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

909 value of the item. 

910 """ 

911 args = [] 

912 if comment is not None: 

913 args.append(comment) 

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

915 

916 def setComment(self, name, comment): 

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

918 

919 Parameters 

920 ---------- 

921 name : `str` 

922 Name of the key to receive updated comment. 

923 comment : `comment` 

924 New comment string. 

925 """ 

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

927 # one that has the new comment 

928 containerType = _propertyContainerElementTypeName(self, name) 

929 if self.isArray(name): 

930 value = self.getArray(name) 

931 else: 

932 value = self.getScalar(name) 

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

934 

935 def toList(self): 

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

937 in the order that they were inserted. 

938 

939 Returns 

940 ------- 

941 ret : `list` of `tuple` 

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

943 in which they were inserted. 

944 """ 

945 orderedNames = self.getOrderedNames() 

946 ret = [] 

947 for name in orderedNames: 

948 if self.isArray(name): 

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

950 for v in values: 

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

952 else: 

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

954 self.getComment(name))) 

955 return ret 

956 

957 def toOrderedDict(self): 

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

959 they were inserted. 

960 

961 Returns 

962 ------- 

963 d : `dict` 

964 Ordered dictionary with all properties in the order that they 

965 were inserted. Comments are not included. 

966 

967 Notes 

968 ----- 

969 As of Python 3.6 dicts retain their insertion order. 

970 """ 

971 d = {} 

972 for name in self: 

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

974 return d 

975 

976 # For PropertyList the two are equivalent 

977 toDict = toOrderedDict 

978 

979 def __eq__(self, other): 

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

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

982 # doesn't either. 

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

984 return False 

985 

986 for name in self: 

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

988 return False 

989 

990 return True 

991 

992 def __copy__(self): 

993 # Copy without having to go through pickle state 

994 pl = PropertyList() 

995 for itemName in self: 

996 pl.copy(itemName, self, itemName) 

997 return pl 

998 

999 def __deepcopy__(self, memo): 

1000 result = self.deepCopy() 

1001 memo[id(self)] = result 

1002 return result 

1003 

1004 def __iter__(self): 

1005 for n in self.getOrderedNames(): 

1006 yield n 

1007 

1008 def __setitem__(self, name, value): 

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

1010 

1011 Parameters 

1012 ---------- 

1013 name : `str` 

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

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

1016 than the value. 

1017 value : Value to assign 

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

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

1020 `PropertySet` before assignment. 

1021 

1022 Notes 

1023 ----- 

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

1025 """ 

1026 if name.endswith(self.COMMENTSUFFIX): 

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

1028 self.setComment(name, value) 

1029 return 

1030 if isinstance(value, Mapping): 

1031 # Create a property set instead 

1032 ps = PropertySet() 

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

1034 ps[k] = v 

1035 value = ps 

1036 self.set(name, value) 

1037 

1038 def __reduce__(self): 

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

1040 # However, implementing __setstate__ in Python causes segfaults 

1041 # because pickle creates a new instance by calling 

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

1043 # the pybind11 memory allocation step. 

1044 return (_makePropertyList, (getPropertyListState(self),))