Hide keyboard shortcuts

Hot-keys 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

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 """A non-inheritance interface for objects that have a string name that 

55 maps directly to their equality comparisons. 

56 """ 

57 @property 

58 def name(self) -> str: 

59 pass 

60 

61 

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

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

64V = TypeVar("V") 

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

66 

67 

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

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

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

71 object are permitted. 

72 

73 Notes 

74 ----- 

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

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

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

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

79 """ 

80 

81 __slots__ = () 

82 

83 @property 

84 @abstractmethod 

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

86 """The set of names associated with the keys, in the same order 

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

88 """ 

89 raise NotImplementedError() 

90 

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

92 """Return a `Mapping` with names as keys and the same values as 

93 ``self``. 

94 

95 Returns 

96 ------- 

97 dictionary : `dict` 

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

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

100 not a view. 

101 """ 

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

103 

104 @abstractmethod 

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

106 # TODO: docs 

107 raise NotImplementedError() 

108 

109 @abstractmethod 

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

111 raise NotImplementedError() 

112 

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

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

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

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

117 

118 

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

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

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

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

123""" 

124 

125 

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

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

128 """ 

129 

130 __slots__ = () 

131 

132 @abstractmethod 

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

134 raise NotImplementedError() 

135 

136 @abstractmethod 

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

138 raise NotImplementedError() 

139 

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

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

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

143 

144 

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

146 """A dictionary wrapper that require keys to have a ``.name`` attribute, 

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

148 

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

150 when adding new items. 

151 

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

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

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

155 dictionary. 

156 

157 Parameters 

158 ---------- 

159 args 

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

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

162 keys for `NamedKeyDict`. 

163 

164 Raises 

165 ------ 

166 AttributeError 

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

168 attribute to the dictionary. 

169 AssertionError 

170 Raised when multiple keys have the same name. 

171 """ 

172 

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

174 

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

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

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

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

179 

180 @property 

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

182 """The set of names associated with the keys, in the same order 

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

184 """ 

185 return self._names.keys() 

186 

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

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

189 """ 

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

191 

192 def __len__(self) -> int: 

193 return len(self._dict) 

194 

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

196 return iter(self._dict) 

197 

198 def __str__(self) -> str: 

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

200 

201 def __repr__(self) -> str: 

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

203 

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

205 if isinstance(key, str): 

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

207 else: 

208 return self._dict[key] 

209 

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

211 if isinstance(key, str): 

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

213 else: 

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

215 self._dict[key] = value 

216 self._names[key.name] = key 

217 

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

219 if isinstance(key, str): 

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

221 del self._names[key] 

222 else: 

223 del self._dict[key] 

224 del self._names[key.name] 

225 

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

227 return NameMappingSetView(self._names) 

228 

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

230 return self._dict.values() 

231 

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

233 return self._dict.items() 

234 

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

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

237 """ 

238 result = NamedKeyDict.__new__(NamedKeyDict) 

239 result._dict = dict(self._dict) 

240 result._names = dict(self._names) 

241 return result 

242 

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

244 """Disable all mutators, effectively transforming ``self`` into 

245 an immutable mapping. 

246 

247 Returns 

248 ------- 

249 self : `NamedKeyMapping` 

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

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

252 to a new variable (and considering any previous references 

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

254 """ 

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

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

257 return self 

258 

259 

260class NamedValueAbstractSet(AbstractSet[K_co]): 

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

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

263 views to be supported. 

264 """ 

265 

266 __slots__ = () 

267 

268 @property 

269 @abstractmethod 

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

271 """The set of names associated with the keys, in the same order 

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

273 """ 

274 raise NotImplementedError() 

275 

276 @abstractmethod 

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

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

279 

280 Returns 

281 ------- 

282 dict : `Mapping` 

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

284 """ 

285 raise NotImplementedError() 

286 

287 @abstractmethod 

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

289 raise NotImplementedError() 

290 

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

292 """Return the element with the given name, or ``default`` if 

293 no such element is present. 

294 """ 

295 try: 

296 return self[key] 

297 except KeyError: 

298 return default 

299 

300 @classmethod 

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

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

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

304 documentation for more information). 

305 

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

307 `NamedValueAbstractSet` instances. 

308 """ 

309 return NamedValueSet(iterable) 

310 

311 

312class NameMappingSetView(NamedValueAbstractSet[K_co]): 

313 """A lightweight implementation of `NamedValueAbstractSet` backed by a 

314 mapping from name to named object. 

315 

316 Parameters 

317 ---------- 

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

319 Mapping this object will provide a view of. 

320 """ 

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

322 self._mapping = mapping 

323 

324 __slots__ = ("_mapping",) 

325 

326 @property 

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

328 # Docstring inherited from NamedValueAbstractSet. 

329 return self._mapping.keys() 

330 

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

332 # Docstring inherited from NamedValueAbstractSet. 

333 return self._mapping 

334 

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

336 if isinstance(key, str): 

337 return self._mapping[key] 

338 else: 

339 return self._mapping[key.name] 

340 

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

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

343 

344 def __len__(self) -> int: 

345 return len(self._mapping) 

346 

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

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

349 

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

351 if isinstance(other, NamedValueAbstractSet): 

352 return self.names == other.names 

353 else: 

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

355 

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

357 if isinstance(other, NamedValueAbstractSet): 

358 return self.names <= other.names 

359 else: 

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

361 

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

363 if isinstance(other, NamedValueAbstractSet): 

364 return self.names >= other.names 

365 else: 

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

367 

368 def __str__(self) -> str: 

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

370 

371 def __repr__(self) -> str: 

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

373 

374 

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

376 """An abstract base class that adds mutation interfaces to 

377 `NamedValueAbstractSet`. 

378 

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

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

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

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

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

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

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

386 `NamedValueAbstractSet.__getitem__`). 

387 """ 

388 

389 __slots__ = () 

390 

391 @abstractmethod 

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

393 raise NotImplementedError() 

394 

395 @abstractmethod 

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

397 """Remove an element from the set. 

398 

399 Parameters 

400 ---------- 

401 element : `object` or `str` 

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

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

404 

405 Raises 

406 ------ 

407 KeyError 

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

409 """ 

410 raise NotImplementedError() 

411 

412 @abstractmethod 

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

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

415 

416 Does nothing if no matching element is present. 

417 

418 Parameters 

419 ---------- 

420 element : `object` or `str` 

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

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

423 """ 

424 raise NotImplementedError() 

425 

426 @abstractmethod 

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

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

429 

430 Parameters 

431 ---------- 

432 name : `str`, optional 

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

434 positionally. If not provided, an arbitrary element is 

435 removed and returned. 

436 

437 Raises 

438 ------ 

439 KeyError 

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

441 matching element exists. 

442 """ 

443 raise NotImplementedError() 

444 

445 

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

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

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

449 

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

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

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

453 and the `get` method. 

454 

455 Parameters 

456 ---------- 

457 elements : `iterable` 

458 Iterable over elements to include in the set. 

459 

460 Raises 

461 ------ 

462 AttributeError 

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

464 

465 Notes 

466 ----- 

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

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

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

470 their iterator order is not the same. 

471 """ 

472 

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

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

475 

476 def __repr__(self) -> str: 

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

478 

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

480 return self <= other 

481 

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

483 return self >= other 

484 

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

486 del self._mapping[name] 

487 

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

489 """Add an element to the set. 

490 

491 Raises 

492 ------ 

493 AttributeError 

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

495 """ 

496 self._mapping[element.name] = element 

497 

498 def clear(self) -> None: 

499 # Docstring inherited. 

500 self._mapping.clear() 

501 

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

503 # Docstring inherited. 

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

505 

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

507 # Docstring inherited. 

508 try: 

509 self.remove(element) 

510 except KeyError: 

511 pass 

512 

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

514 # Docstring inherited. 

515 if not args: 

516 return super().pop() 

517 else: 

518 return self._mapping.pop(*args) 

519 

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

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

522 

523 Parameters 

524 ---------- 

525 elements : `Iterable` 

526 Elements to add. 

527 """ 

528 for element in elements: 

529 self.add(element) 

530 

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

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

533 """ 

534 result = NamedValueSet.__new__(NamedValueSet) 

535 result._mapping = dict(self._mapping) 

536 return result 

537 

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

539 """Disable all mutators, effectively transforming ``self`` into 

540 an immutable set. 

541 

542 Returns 

543 ------- 

544 self : `NamedValueAbstractSet` 

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

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

547 to a new variable (and considering any previous references 

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

549 """ 

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

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

552 return self 

553 

554 _mapping: Dict[str, K]