Coverage for python/lsst/daf/butler/core/named.py: 45%

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

197 statements  

1# This file is part of daf_butler. 

2# 

3# Developed for the LSST Data Management System. 

4# This product includes software developed by the LSST Project 

5# (http://www.lsst.org). 

6# See the COPYRIGHT file at the top-level directory of this distribution 

7# for details of code ownership. 

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 GNU General Public License 

20# along with this program. If not, see <http://www.gnu.org/licenses/>. 

21from __future__ import annotations 

22 

23__all__ = ( 

24 "NamedKeyDict", 

25 "NamedKeyMapping", 

26 "NamedValueAbstractSet", 

27 "NamedValueMutableSet", 

28 "NamedValueSet", 

29 "NameLookupMapping", 

30 "NameMappingSetView", 

31) 

32 

33from abc import abstractmethod 

34from typing import ( 

35 AbstractSet, 

36 Any, 

37 Dict, 

38 ItemsView, 

39 Iterable, 

40 Iterator, 

41 KeysView, 

42 Mapping, 

43 MutableMapping, 

44 MutableSet, 

45 Protocol, 

46 TypeVar, 

47 Union, 

48 ValuesView, 

49) 

50from types import MappingProxyType 

51 

52 

53class Named(Protocol): 

54 """Protocol for objects with string name. 

55 

56 A non-inheritance interface for objects that have a string name that 

57 maps directly to their equality comparisons. 

58 """ 

59 

60 @property 

61 def name(self) -> str: 

62 pass 

63 

64 

65K = TypeVar("K", bound=Named) 

66K_co = TypeVar("K_co", bound=Named, covariant=True) 

67V = TypeVar("V") 

68V_co = TypeVar("V_co", covariant=True) 

69 

70 

71class NamedKeyMapping(Mapping[K_co, V_co]): 

72 """Custom mapping class. 

73 

74 An abstract base class for custom mappings whose keys are objects with 

75 a `str` ``name`` attribute, for which lookups on the name as well as the 

76 object are permitted. 

77 

78 Notes 

79 ----- 

80 In addition to the new `names` property and `byName` method, this class 

81 simply redefines the type signature for `__getitem__` and `get` that would 

82 otherwise be inherited from `Mapping`. That is only relevant for static 

83 type checking; the actual Python runtime doesn't care about types at all. 

84 """ 

85 

86 __slots__ = () 

87 

88 @property 

89 @abstractmethod 

90 def names(self) -> AbstractSet[str]: 

91 """Return the set of names associated with the keys, in the same order. 

92 

93 (`AbstractSet` [ `str` ]). 

94 """ 

95 raise NotImplementedError() 

96 

97 def byName(self) -> Dict[str, V_co]: 

98 """Return a `Mapping` with names as keys and the ``self`` values. 

99 

100 Returns 

101 ------- 

102 dictionary : `dict` 

103 A dictionary with the same values (and iteration order) as 

104 ``self``, with `str` names as keys. This is always a new object, 

105 not a view. 

106 """ 

107 return dict(zip(self.names, self.values())) 

108 

109 @abstractmethod 

110 def keys(self) -> NamedValueAbstractSet[K_co]: 

111 # TODO: docs 

112 raise NotImplementedError() 

113 

114 @abstractmethod 

115 def __getitem__(self, key: Union[str, K_co]) -> V_co: 

116 raise NotImplementedError() 

117 

118 def get(self, key: Union[str, K_co], default: Any = None) -> Any: 

119 # Delegating to super is not allowed by typing, because it doesn't 

120 # accept str, but we know it just delegates to __getitem__, which does. 

121 return super().get(key, default) # type: ignore 

122 

123 

124NameLookupMapping = Union[NamedKeyMapping[K_co, V_co], Mapping[str, V_co]] 

125"""A type annotation alias for signatures that want to use ``mapping[s]`` 

126(or ``mapping.get(s)``) where ``s`` is a `str`, and don't care whether 

127``mapping.keys()`` returns named objects or direct `str` instances. 

128""" 

129 

130 

131class NamedKeyMutableMapping(NamedKeyMapping[K, V], MutableMapping[K, V]): 

132 """An abstract base class that adds mutation to `NamedKeyMapping`.""" 

133 

134 __slots__ = () 

135 

136 @abstractmethod 

137 def __setitem__(self, key: Union[str, K], value: V) -> None: 

138 raise NotImplementedError() 

139 

140 @abstractmethod 

141 def __delitem__(self, key: Union[str, K]) -> None: 

142 raise NotImplementedError() 

143 

144 def pop(self, key: Union[str, K], default: Any = None) -> Any: 

145 # See comment in `NamedKeyMapping.get`; same logic applies here. 

146 return super().pop(key, default) # type: ignore 

147 

148 

149class NamedKeyDict(NamedKeyMutableMapping[K, V]): 

150 """Dictionary wrapper for named keys. 

151 

152 Requires keys to have a ``.name`` attribute, 

153 and permits lookups using either key objects or their names. 

154 

155 Names can be used in place of keys when updating existing items, but not 

156 when adding new items. 

157 

158 It is assumed (but asserted) that all name equality is equivalent to key 

159 equality, either because the key objects define equality this way, or 

160 because different objects with the same name are never included in the same 

161 dictionary. 

162 

163 Parameters 

164 ---------- 

165 args 

166 All positional constructor arguments are forwarded directly to `dict`. 

167 Keyword arguments are not accepted, because plain strings are not valid 

168 keys for `NamedKeyDict`. 

169 

170 Raises 

171 ------ 

172 AttributeError 

173 Raised when an attempt is made to add an object with no ``.name`` 

174 attribute to the dictionary. 

175 AssertionError 

176 Raised when multiple keys have the same name. 

177 """ 

178 

179 __slots__ = ("_dict", "_names",) 

180 

181 def __init__(self, *args: Any): 

182 self._dict: Dict[K, V] = dict(*args) 

183 self._names = {key.name: key for key in self._dict} 

184 assert len(self._names) == len(self._dict), "Duplicate names in keys." 

185 

186 @property 

187 def names(self) -> KeysView[str]: 

188 """Return set of names associated with the keys, in the same order. 

189 

190 (`~collections.abc.KeysView`). 

191 """ 

192 return self._names.keys() 

193 

194 def byName(self) -> Dict[str, V]: 

195 """Return a `dict` with names as keys and the ``self`` values.""" 

196 return dict(zip(self._names.keys(), self._dict.values())) 

197 

198 def __len__(self) -> int: 

199 return len(self._dict) 

200 

201 def __iter__(self) -> Iterator[K]: 

202 return iter(self._dict) 

203 

204 def __str__(self) -> str: 

205 return "{{{}}}".format(", ".join(f"{str(k)}: {str(v)}" for k, v in self.items())) 

206 

207 def __repr__(self) -> str: 

208 return "NamedKeyDict({{{}}})".format(", ".join(f"{repr(k)}: {repr(v)}" for k, v in self.items())) 

209 

210 def __getitem__(self, key: Union[str, K]) -> V: 

211 if isinstance(key, str): 

212 return self._dict[self._names[key]] 

213 else: 

214 return self._dict[key] 

215 

216 def __setitem__(self, key: Union[str, K], value: V) -> None: 

217 if isinstance(key, str): 

218 self._dict[self._names[key]] = value 

219 else: 

220 assert self._names.get(key.name, key) == key, "Name is already associated with a different key." 

221 self._dict[key] = value 

222 self._names[key.name] = key 

223 

224 def __delitem__(self, key: Union[str, K]) -> None: 

225 if isinstance(key, str): 

226 del self._dict[self._names[key]] 

227 del self._names[key] 

228 else: 

229 del self._dict[key] 

230 del self._names[key.name] 

231 

232 def keys(self) -> NamedValueAbstractSet[K]: 

233 return NameMappingSetView(self._names) 

234 

235 def values(self) -> ValuesView[V]: 

236 return self._dict.values() 

237 

238 def items(self) -> ItemsView[K, V]: 

239 return self._dict.items() 

240 

241 def copy(self) -> NamedKeyDict[K, V]: 

242 """Return a new `NamedKeyDict` with the same elements.""" 

243 result = NamedKeyDict.__new__(NamedKeyDict) 

244 result._dict = dict(self._dict) 

245 result._names = dict(self._names) 

246 return result 

247 

248 def freeze(self) -> NamedKeyMapping[K, V]: 

249 """Disable all mutators. 

250 

251 Effectively transforms ``self`` into an immutable mapping. 

252 

253 Returns 

254 ------- 

255 self : `NamedKeyMapping` 

256 While ``self`` is modified in-place, it is also returned with a 

257 type anotation that reflects its new, frozen state; assigning it 

258 to a new variable (and considering any previous references 

259 invalidated) should allow for more accurate static type checking. 

260 """ 

261 if not isinstance(self._dict, MappingProxyType): 

262 self._dict = MappingProxyType(self._dict) # type: ignore 

263 return self 

264 

265 

266class NamedValueAbstractSet(AbstractSet[K_co]): 

267 """Custom sets with named elements. 

268 

269 An abstract base class for custom sets whose elements are objects with 

270 a `str` ``name`` attribute, allowing some dict-like operations and 

271 views to be supported. 

272 """ 

273 

274 __slots__ = () 

275 

276 @property 

277 @abstractmethod 

278 def names(self) -> AbstractSet[str]: 

279 """Return set of names associated with the keys, in the same order. 

280 

281 (`AbstractSet` [ `str` ]). 

282 """ 

283 raise NotImplementedError() 

284 

285 @abstractmethod 

286 def asMapping(self) -> Mapping[str, K_co]: 

287 """Return a mapping view with names as keys. 

288 

289 Returns 

290 ------- 

291 dict : `Mapping` 

292 A dictionary-like view with ``values() == self``. 

293 """ 

294 raise NotImplementedError() 

295 

296 @abstractmethod 

297 def __getitem__(self, key: Union[str, K_co]) -> K_co: 

298 raise NotImplementedError() 

299 

300 def get(self, key: Union[str, K_co], default: Any = None) -> Any: 

301 """Return the element with the given name. 

302 

303 Returns ``default`` if no such element is present. 

304 """ 

305 try: 

306 return self[key] 

307 except KeyError: 

308 return default 

309 

310 @classmethod 

311 def _from_iterable(cls, iterable: Iterable[K_co]) -> NamedValueSet[K_co]: 

312 """Construct class from an iterable. 

313 

314 Hook to ensure that inherited `collections.abc.Set` operators return 

315 `NamedValueSet` instances, not something else (see `collections.abc` 

316 documentation for more information). 

317 

318 Note that this behavior can only be guaranteed when both operands are 

319 `NamedValueAbstractSet` instances. 

320 """ 

321 return NamedValueSet(iterable) 

322 

323 

324class NameMappingSetView(NamedValueAbstractSet[K_co]): 

325 """A lightweight implementation of `NamedValueAbstractSet`. 

326 

327 Backed by a mapping from name to named object. 

328 

329 Parameters 

330 ---------- 

331 mapping : `Mapping` [ `str`, `object` ] 

332 Mapping this object will provide a view of. 

333 """ 

334 

335 def __init__(self, mapping: Mapping[str, K_co]): 

336 self._mapping = mapping 

337 

338 __slots__ = ("_mapping",) 

339 

340 @property 

341 def names(self) -> AbstractSet[str]: 

342 # Docstring inherited from NamedValueAbstractSet. 

343 return self._mapping.keys() 

344 

345 def asMapping(self) -> Mapping[str, K_co]: 

346 # Docstring inherited from NamedValueAbstractSet. 

347 return self._mapping 

348 

349 def __getitem__(self, key: Union[str, K_co]) -> K_co: 

350 if isinstance(key, str): 

351 return self._mapping[key] 

352 else: 

353 return self._mapping[key.name] 

354 

355 def __contains__(self, key: Any) -> bool: 

356 return getattr(key, "name", key) in self._mapping 

357 

358 def __len__(self) -> int: 

359 return len(self._mapping) 

360 

361 def __iter__(self) -> Iterator[K_co]: 

362 return iter(self._mapping.values()) 

363 

364 def __eq__(self, other: Any) -> bool: 

365 if isinstance(other, NamedValueAbstractSet): 

366 return self.names == other.names 

367 else: 

368 return set(self._mapping.values()) == other 

369 

370 def __le__(self, other: AbstractSet[K]) -> bool: 

371 if isinstance(other, NamedValueAbstractSet): 

372 return self.names <= other.names 

373 else: 

374 return set(self._mapping.values()) <= other 

375 

376 def __ge__(self, other: AbstractSet[K]) -> bool: 

377 if isinstance(other, NamedValueAbstractSet): 

378 return self.names >= other.names 

379 else: 

380 return set(self._mapping.values()) >= other 

381 

382 def __str__(self) -> str: 

383 return "{{{}}}".format(", ".join(str(element) for element in self)) 

384 

385 def __repr__(self) -> str: 

386 return f"NameMappingSetView({self._mapping})" 

387 

388 

389class NamedValueMutableSet(NamedValueAbstractSet[K], MutableSet[K]): 

390 """Mutable variant of `NamedValueAbstractSet`. 

391 

392 Methods that can add new elements to the set are unchanged from their 

393 `MutableSet` definitions, while those that only remove them can generally 

394 accept names or element instances. `pop` can be used in either its 

395 `MutableSet` form (no arguments; an arbitrary element is returned) or its 

396 `MutableMapping` form (one or two arguments for the name and optional 

397 default value, respectively). A `MutableMapping`-like `__delitem__` 

398 interface is also included, which takes only names (like 

399 `NamedValueAbstractSet.__getitem__`). 

400 """ 

401 

402 __slots__ = () 

403 

404 @abstractmethod 

405 def __delitem__(self, name: str) -> None: 

406 raise NotImplementedError() 

407 

408 @abstractmethod 

409 def remove(self, element: Union[str, K]) -> Any: 

410 """Remove an element from the set. 

411 

412 Parameters 

413 ---------- 

414 element : `object` or `str` 

415 Element to remove or the string name thereof. Assumed to be an 

416 element if it has a ``.name`` attribute. 

417 

418 Raises 

419 ------ 

420 KeyError 

421 Raised if an element with the given name does not exist. 

422 """ 

423 raise NotImplementedError() 

424 

425 @abstractmethod 

426 def discard(self, element: Union[str, K]) -> Any: 

427 """Remove an element from the set if it exists. 

428 

429 Does nothing if no matching element is present. 

430 

431 Parameters 

432 ---------- 

433 element : `object` or `str` 

434 Element to remove or the string name thereof. Assumed to be an 

435 element if it has a ``.name`` attribute. 

436 """ 

437 raise NotImplementedError() 

438 

439 @abstractmethod 

440 def pop(self, *args: str) -> K: 

441 """Remove and return an element from the set. 

442 

443 Parameters 

444 ---------- 

445 name : `str`, optional 

446 Name of the element to remove and return. Must be passed 

447 positionally. If not provided, an arbitrary element is 

448 removed and returned. 

449 

450 Raises 

451 ------ 

452 KeyError 

453 Raised if ``name`` is provided but ``default`` is not, and no 

454 matching element exists. 

455 """ 

456 raise NotImplementedError() 

457 

458 

459class NamedValueSet(NameMappingSetView[K], NamedValueMutableSet[K]): 

460 """Custom mutable set class. 

461 

462 A custom mutable set class that requires elements to have a ``.name`` 

463 attribute, which can then be used as keys in `dict`-like lookup. 

464 

465 Names and elements can both be used with the ``in`` and ``del`` 

466 operators, `remove`, and `discard`. Names (but not elements) 

467 can be used with ``[]``-based element retrieval (not assignment) 

468 and the `get` method. 

469 

470 Parameters 

471 ---------- 

472 elements : `iterable` 

473 Iterable over elements to include in the set. 

474 

475 Raises 

476 ------ 

477 AttributeError 

478 Raised if one or more elements do not have a ``.name`` attribute. 

479 

480 Notes 

481 ----- 

482 Iteration order is guaranteed to be the same as insertion order (with 

483 the same general behavior as `dict` ordering). 

484 Like `dicts`, sets with the same elements will compare as equal even if 

485 their iterator order is not the same. 

486 """ 

487 

488 def __init__(self, elements: Iterable[K] = ()): 

489 super().__init__({element.name: element for element in elements}) 

490 

491 def __repr__(self) -> str: 

492 return "NamedValueSet({{{}}})".format(", ".join(repr(element) for element in self)) 

493 

494 def issubset(self, other: AbstractSet[K]) -> bool: 

495 return self <= other 

496 

497 def issuperset(self, other: AbstractSet[K]) -> bool: 

498 return self >= other 

499 

500 def __delitem__(self, name: str) -> None: 

501 del self._mapping[name] 

502 

503 def add(self, element: K) -> None: 

504 """Add an element to the set. 

505 

506 Raises 

507 ------ 

508 AttributeError 

509 Raised if the element does not have a ``.name`` attribute. 

510 """ 

511 self._mapping[element.name] = element 

512 

513 def clear(self) -> None: 

514 # Docstring inherited. 

515 self._mapping.clear() 

516 

517 def remove(self, element: Union[str, K]) -> Any: 

518 # Docstring inherited. 

519 del self._mapping[getattr(element, "name", element)] 

520 

521 def discard(self, element: Union[str, K]) -> Any: 

522 # Docstring inherited. 

523 try: 

524 self.remove(element) 

525 except KeyError: 

526 pass 

527 

528 def pop(self, *args: str) -> K: 

529 # Docstring inherited. 

530 if not args: 

531 return super().pop() 

532 else: 

533 return self._mapping.pop(*args) 

534 

535 def update(self, elements: Iterable[K]) -> None: 

536 """Add multiple new elements to the set. 

537 

538 Parameters 

539 ---------- 

540 elements : `Iterable` 

541 Elements to add. 

542 """ 

543 for element in elements: 

544 self.add(element) 

545 

546 def copy(self) -> NamedValueSet[K]: 

547 """Return a new `NamedValueSet` with the same elements.""" 

548 result = NamedValueSet.__new__(NamedValueSet) 

549 result._mapping = dict(self._mapping) 

550 return result 

551 

552 def freeze(self) -> NamedValueAbstractSet[K]: 

553 """Disable all mutators. 

554 

555 Effectively transforming ``self`` into an immutable set. 

556 

557 Returns 

558 ------- 

559 self : `NamedValueAbstractSet` 

560 While ``self`` is modified in-place, it is also returned with a 

561 type anotation that reflects its new, frozen state; assigning it 

562 to a new variable (and considering any previous references 

563 invalidated) should allow for more accurate static type checking. 

564 """ 

565 if not isinstance(self._mapping, MappingProxyType): 

566 self._mapping = MappingProxyType(self._mapping) # type: ignore 

567 return self 

568 

569 _mapping: Dict[str, K]