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 TypeVar, 

46 Union, 

47 ValuesView, 

48) 

49from types import MappingProxyType 

50try: 

51 # If we're running mypy, we should have typing_extensions. 

52 # If we aren't running mypy, we shouldn't assume we do. 

53 # When we're safely on Python 3.8, we can import Protocol 

54 # from typing and avoid all of this. 

55 from typing_extensions import Protocol 

56 

57 class Named(Protocol): 

58 @property 

59 def name(self) -> str: 

60 pass 

61 

62except ImportError: 

63 Named = Any # type: ignore 

64 

65 

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

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

68V = TypeVar("V") 

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

70 

71 

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

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

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

75 object are permitted. 

76 

77 Notes 

78 ----- 

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

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

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

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

83 """ 

84 

85 __slots__ = () 

86 

87 @property 

88 @abstractmethod 

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

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

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

92 """ 

93 raise NotImplementedError() 

94 

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

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

97 ``self``. 

98 

99 Returns 

100 ------- 

101 dictionary : `dict` 

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

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

104 not a view. 

105 """ 

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

107 

108 @abstractmethod 

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

110 # TODO: docs 

111 raise NotImplementedError() 

112 

113 @abstractmethod 

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

115 raise NotImplementedError() 

116 

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

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

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

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

121 

122 

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

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

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

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

127""" 

128 

129 

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

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

132 """ 

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 """A dictionary wrapper that require keys to have a ``.name`` attribute, 

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

152 

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

154 when adding new items. 

155 

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

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

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

159 dictionary. 

160 

161 Parameters 

162 ---------- 

163 args 

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

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

166 keys for `NamedKeyDict`. 

167 

168 Raises 

169 ------ 

170 AttributeError 

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

172 attribute to the dictionary. 

173 AssertionError 

174 Raised when multiple keys have the same name. 

175 """ 

176 

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

178 

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

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

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

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

183 

184 @property 

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

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

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

188 """ 

189 return self._names.keys() 

190 

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

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

193 """ 

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

195 

196 def __len__(self) -> int: 

197 return len(self._dict) 

198 

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

200 return iter(self._dict) 

201 

202 def __str__(self) -> str: 

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

204 

205 def __repr__(self) -> str: 

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

207 

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

209 if isinstance(key, str): 

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

211 else: 

212 return self._dict[key] 

213 

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

215 if isinstance(key, str): 

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

217 else: 

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

219 self._dict[key] = value 

220 self._names[key.name] = key 

221 

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

223 if isinstance(key, str): 

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

225 del self._names[key] 

226 else: 

227 del self._dict[key] 

228 del self._names[key.name] 

229 

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

231 return NameMappingSetView(self._names) 

232 

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

234 return self._dict.values() 

235 

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

237 return self._dict.items() 

238 

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

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

241 """ 

242 result = NamedKeyDict.__new__(NamedKeyDict) 

243 result._dict = dict(self._dict) 

244 result._names = dict(self._names) 

245 return result 

246 

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

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

249 an immutable mapping. 

250 

251 Returns 

252 ------- 

253 self : `NamedKeyMapping` 

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

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

256 to a new variable (and considering any previous references 

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

258 """ 

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

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

261 return self 

262 

263 

264class NamedValueAbstractSet(AbstractSet[K_co]): 

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

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

267 views to be supported. 

268 """ 

269 

270 __slots__ = () 

271 

272 @property 

273 @abstractmethod 

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

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

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

277 """ 

278 raise NotImplementedError() 

279 

280 @abstractmethod 

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

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

283 

284 Returns 

285 ------- 

286 dict : `Mapping` 

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

288 """ 

289 raise NotImplementedError() 

290 

291 @abstractmethod 

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

293 raise NotImplementedError() 

294 

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

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

297 no such element is present. 

298 """ 

299 try: 

300 return self[key] 

301 except KeyError: 

302 return default 

303 

304 

305class NameMappingSetView(NamedValueAbstractSet[K_co]): 

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

307 mapping from name to named object. 

308 

309 Parameters 

310 ---------- 

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

312 Mapping this object will provide a view of. 

313 """ 

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

315 self._mapping = mapping 

316 

317 __slots__ = ("_mapping",) 

318 

319 @property 

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

321 # Docstring inherited from NamedValueAbstractSet. 

322 return self._mapping.keys() 

323 

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

325 # Docstring inherited from NamedValueAbstractSet. 

326 return self._mapping 

327 

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

329 if isinstance(key, str): 

330 return self._mapping[key] 

331 else: 

332 return self._mapping[key.name] 

333 

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

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

336 

337 def __len__(self) -> int: 

338 return len(self._mapping) 

339 

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

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

342 

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

344 if isinstance(other, NamedValueAbstractSet): 

345 return self.names == other.names 

346 else: 

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

348 

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

350 if isinstance(other, NamedValueAbstractSet): 

351 return self.names <= other.names 

352 else: 

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

354 

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

356 if isinstance(other, NamedValueAbstractSet): 

357 return self.names >= other.names 

358 else: 

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

360 

361 def __str__(self) -> str: 

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

363 

364 def __repr__(self) -> str: 

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

366 

367 

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

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

370 `NamedValueAbstractSet`. 

371 

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

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

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

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

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

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

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

379 `NamedValueAbstractSet.__getitem__`). 

380 """ 

381 

382 __slots__ = () 

383 

384 @abstractmethod 

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

386 raise NotImplementedError() 

387 

388 @abstractmethod 

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

390 """Remove an element from the set. 

391 

392 Parameters 

393 ---------- 

394 element : `object` or `str` 

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

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

397 

398 Raises 

399 ------ 

400 KeyError 

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

402 """ 

403 raise NotImplementedError() 

404 

405 @abstractmethod 

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

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

408 

409 Does nothing if no matching element is present. 

410 

411 Parameters 

412 ---------- 

413 element : `object` or `str` 

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

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

416 """ 

417 raise NotImplementedError() 

418 

419 @abstractmethod 

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

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

422 

423 Parameters 

424 ---------- 

425 name : `str`, optional 

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

427 positionally. If not provided, an arbitrary element is 

428 removed and returned. 

429 

430 Raises 

431 ------ 

432 KeyError 

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

434 matching element exists. 

435 """ 

436 raise NotImplementedError() 

437 

438 

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

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

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

442 

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

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

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

446 and the `get` method. 

447 

448 Parameters 

449 ---------- 

450 elements : `iterable` 

451 Iterable over elements to include in the set. 

452 

453 Raises 

454 ------ 

455 AttributeError 

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

457 

458 Notes 

459 ----- 

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

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

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

463 their iterator order is not the same. 

464 """ 

465 

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

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

468 

469 def __repr__(self) -> str: 

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

471 

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

473 return self <= other 

474 

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

476 return self >= other 

477 

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

479 del self._mapping[name] 

480 

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

482 """Add an element to the set. 

483 

484 Raises 

485 ------ 

486 AttributeError 

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

488 """ 

489 self._mapping[element.name] = element 

490 

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

492 # Docstring inherited. 

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

494 

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

496 # Docstring inherited. 

497 try: 

498 self.remove(element) 

499 except KeyError: 

500 pass 

501 

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

503 # Docstring inherited. 

504 if not args: 

505 return super().pop() 

506 else: 

507 return self._mapping.pop(*args) 

508 

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

510 """Add multple new elements to the set. 

511 

512 Parameters 

513 ---------- 

514 elements : `Iterable` 

515 Elements to add. 

516 """ 

517 for element in elements: 

518 self.add(element) 

519 

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

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

522 """ 

523 result = NamedValueSet.__new__(NamedValueSet) 

524 result._mapping = dict(self._mapping) 

525 return result 

526 

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

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

529 an immutable set. 

530 

531 Returns 

532 ------- 

533 self : `NamedValueAbstractSet` 

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

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

536 to a new variable (and considering any previous references 

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

538 """ 

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

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

541 return self 

542 

543 _mapping: Dict[str, K]