Coverage for python/lsst/daf/butler/core/dimensions/_dataCoordinateIterable.py: 31%

204 statements  

« prev     ^ index     » next       coverage.py v7.2.5, created at 2023-05-02 09:50 +0000

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/>. 

21 

22from __future__ import annotations 

23 

24__all__ = ( 

25 "DataCoordinateIterable", 

26 "DataCoordinateSet", 

27 "DataCoordinateSequence", 

28) 

29 

30from abc import abstractmethod 

31from typing import AbstractSet, Any, Collection, Dict, Iterable, Iterator, Optional, Sequence, overload 

32 

33from ._coordinate import DataCoordinate 

34from ._graph import DimensionGraph 

35from ._universe import DimensionUniverse 

36 

37 

38class DataCoordinateIterable(Iterable[DataCoordinate]): 

39 """An abstract base class for homogeneous iterables of data IDs. 

40 

41 All elements of a `DataCoordinateIterable` identify the same set of 

42 dimensions (given by the `graph` property) and generally have the same 

43 `DataCoordinate.hasFull` and `DataCoordinate.hasRecords` flag values. 

44 """ 

45 

46 __slots__ = () 

47 

48 @staticmethod 

49 def fromScalar(dataId: DataCoordinate) -> _ScalarDataCoordinateIterable: 

50 """Return a `DataCoordinateIterable` containing the single data ID. 

51 

52 Parameters 

53 ---------- 

54 dataId : `DataCoordinate` 

55 Data ID to adapt. Must be a true `DataCoordinate` instance, not 

56 an arbitrary mapping. No runtime checking is performed. 

57 

58 Returns 

59 ------- 

60 iterable : `DataCoordinateIterable` 

61 A `DataCoordinateIterable` instance of unspecified (i.e. 

62 implementation-detail) subclass. Guaranteed to implement 

63 the `collections.abc.Sized` (i.e. `__len__`) and 

64 `collections.abc.Container` (i.e. `__contains__`) interfaces as 

65 well as that of `DataCoordinateIterable`. 

66 """ 

67 return _ScalarDataCoordinateIterable(dataId) 

68 

69 @property 

70 @abstractmethod 

71 def graph(self) -> DimensionGraph: 

72 """Dimensions identified by these data IDs (`DimensionGraph`).""" 

73 raise NotImplementedError() 

74 

75 @property 

76 def universe(self) -> DimensionUniverse: 

77 """Universe that defines all known compatible dimensions. 

78 

79 (`DimensionUniverse`). 

80 """ 

81 return self.graph.universe 

82 

83 @abstractmethod 

84 def hasFull(self) -> bool: 

85 """Indicate if all data IDs in this iterable identify all dimensions. 

86 

87 Not just required dimensions. 

88 

89 Returns 

90 ------- 

91 state : `bool` 

92 If `True`, ``all(d.hasFull() for d in iterable)`` is guaranteed. 

93 If `False`, no guarantees are made. 

94 """ 

95 raise NotImplementedError() 

96 

97 @abstractmethod 

98 def hasRecords(self) -> bool: 

99 """Return whether all data IDs in this iterable contain records. 

100 

101 Returns 

102 ------- 

103 state : `bool` 

104 If `True`, ``all(d.hasRecords() for d in iterable)`` is guaranteed. 

105 If `False`, no guarantees are made. 

106 """ 

107 raise NotImplementedError() 

108 

109 def toSet(self) -> DataCoordinateSet: 

110 """Transform this iterable into a `DataCoordinateSet`. 

111 

112 Returns 

113 ------- 

114 set : `DataCoordinateSet` 

115 A `DatasetCoordinateSet` instance with the same elements as 

116 ``self``, after removing any duplicates. May be ``self`` if it is 

117 already a `DataCoordinateSet`. 

118 """ 

119 return DataCoordinateSet( 

120 frozenset(self), 

121 graph=self.graph, 

122 hasFull=self.hasFull(), 

123 hasRecords=self.hasRecords(), 

124 check=False, 

125 ) 

126 

127 def toSequence(self) -> DataCoordinateSequence: 

128 """Transform this iterable into a `DataCoordinateSequence`. 

129 

130 Returns 

131 ------- 

132 seq : `DataCoordinateSequence` 

133 A new `DatasetCoordinateSequence` with the same elements as 

134 ``self``, in the same order. May be ``self`` if it is already a 

135 `DataCoordinateSequence`. 

136 """ 

137 return DataCoordinateSequence( 

138 tuple(self), graph=self.graph, hasFull=self.hasFull(), hasRecords=self.hasRecords(), check=False 

139 ) 

140 

141 @abstractmethod 

142 def subset(self, graph: DimensionGraph) -> DataCoordinateIterable: 

143 """Return a subset iterable. 

144 

145 This subset iterable returns data IDs that identify a subset of the 

146 dimensions that this one's do. 

147 

148 Parameters 

149 ---------- 

150 graph : `DimensionGraph` 

151 Dimensions to be identified by the data IDs in the returned 

152 iterable. Must be a subset of ``self.graph``. 

153 

154 Returns 

155 ------- 

156 iterable : `DataCoordinateIterable` 

157 A `DataCoordinateIterable` with ``iterable.graph == graph``. 

158 May be ``self`` if ``graph == self.graph``. Elements are 

159 equivalent to those that would be created by calling 

160 `DataCoordinate.subset` on all elements in ``self``, possibly 

161 with deduplication and/or reordering (depending on the subclass, 

162 which may make more specific guarantees). 

163 """ 

164 raise NotImplementedError() 

165 

166 

167class _ScalarDataCoordinateIterable(DataCoordinateIterable): 

168 """An iterable for a single `DataCoordinate`. 

169 

170 A `DataCoordinateIterable` implementation that adapts a single 

171 `DataCoordinate` instance. 

172 

173 This class should only be used directly by other code in the module in 

174 which it is defined; all other code should interact with it only through 

175 the `DataCoordinateIterable` interface. 

176 

177 Parameters 

178 ---------- 

179 dataId : `DataCoordinate` 

180 The data ID to adapt. 

181 """ 

182 

183 def __init__(self, dataId: DataCoordinate): 

184 self._dataId = dataId 

185 

186 __slots__ = ("_dataId",) 

187 

188 def __iter__(self) -> Iterator[DataCoordinate]: 

189 yield self._dataId 

190 

191 def __len__(self) -> int: 

192 return 1 

193 

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

195 if isinstance(key, DataCoordinate): 

196 return key == self._dataId 

197 else: 

198 return False 

199 

200 @property 

201 def graph(self) -> DimensionGraph: 

202 # Docstring inherited from DataCoordinateIterable. 

203 return self._dataId.graph 

204 

205 def hasFull(self) -> bool: 

206 # Docstring inherited from DataCoordinateIterable. 

207 return self._dataId.hasFull() 

208 

209 def hasRecords(self) -> bool: 

210 # Docstring inherited from DataCoordinateIterable. 

211 return self._dataId.hasRecords() 

212 

213 def subset(self, graph: DimensionGraph) -> _ScalarDataCoordinateIterable: 

214 # Docstring inherited from DataCoordinateIterable. 

215 return _ScalarDataCoordinateIterable(self._dataId.subset(graph)) 

216 

217 

218class _DataCoordinateCollectionBase(DataCoordinateIterable): 

219 """A partial iterable implementation backed by native Python collection. 

220 

221 A partial `DataCoordinateIterable` implementation that is backed by a 

222 native Python collection. 

223 

224 This class is intended only to be used as an intermediate base class for 

225 `DataCoordinateIterables` that assume a more specific type of collection 

226 and can hence make more informed choices for how to implement some methods. 

227 

228 Parameters 

229 ---------- 

230 dataIds : `collections.abc.Collection` [ `DataCoordinate` ] 

231 A collection of `DataCoordinate` instances, with dimensions equal to 

232 ``graph``. 

233 graph : `DimensionGraph` 

234 Dimensions identified by all data IDs in the set. 

235 hasFull : `bool`, optional 

236 If `True`, the caller guarantees that `DataCoordinate.hasFull` returns 

237 `True` for all given data IDs. If `False`, no such guarantee is made, 

238 and `hasFull` will always return `False`. If `None` (default), 

239 `hasFull` will be computed from the given data IDs, immediately if 

240 ``check`` is `True`, or on first use if ``check`` is `False`. 

241 hasRecords : `bool`, optional 

242 If `True`, the caller guarantees that `DataCoordinate.hasRecords` 

243 returns `True` for all given data IDs. If `False`, no such guarantee 

244 is made and `hasRecords` will always return `False`. If `None` 

245 (default), `hasRecords` will be computed from the given data IDs, 

246 immediately if ``check`` is `True`, or on first use if ``check`` is 

247 `False`. 

248 check: `bool`, optional 

249 If `True` (default) check that all data IDs are consistent with the 

250 given ``graph`` and state flags at construction. If `False`, no 

251 checking will occur. 

252 """ 

253 

254 def __init__( 

255 self, 

256 dataIds: Collection[DataCoordinate], 

257 graph: DimensionGraph, 

258 *, 

259 hasFull: Optional[bool] = None, 

260 hasRecords: Optional[bool] = None, 

261 check: bool = True, 

262 ): 

263 self._dataIds = dataIds 

264 self._graph = graph 

265 if check: 

266 for dataId in self._dataIds: 

267 if hasFull and not dataId.hasFull(): 

268 raise ValueError(f"{dataId} is not complete, but is required to be.") 

269 if hasRecords and not dataId.hasRecords(): 

270 raise ValueError(f"{dataId} has no records, but is required to.") 

271 if dataId.graph != self._graph: 

272 raise ValueError(f"Bad dimensions {dataId.graph}; expected {self._graph}.") 

273 if hasFull is None: 

274 hasFull = all(dataId.hasFull() for dataId in self._dataIds) 

275 if hasRecords is None: 

276 hasRecords = all(dataId.hasRecords() for dataId in self._dataIds) 

277 self._hasFull = hasFull 

278 self._hasRecords = hasRecords 

279 

280 __slots__ = ("_graph", "_dataIds", "_hasFull", "_hasRecords") 

281 

282 @property 

283 def graph(self) -> DimensionGraph: 

284 # Docstring inherited from DataCoordinateIterable. 

285 return self._graph 

286 

287 def hasFull(self) -> bool: 

288 # Docstring inherited from DataCoordinateIterable. 

289 if self._hasFull is None: 

290 self._hasFull = all(dataId.hasFull() for dataId in self._dataIds) 

291 return self._hasFull 

292 

293 def hasRecords(self) -> bool: 

294 # Docstring inherited from DataCoordinateIterable. 

295 if self._hasRecords is None: 

296 self._hasRecords = all(dataId.hasRecords() for dataId in self._dataIds) 

297 return self._hasRecords 

298 

299 def toSet(self) -> DataCoordinateSet: 

300 # Docstring inherited from DataCoordinateIterable. 

301 # Override base class to pass in attributes instead of results of 

302 # method calls for _hasFull and _hasRecords - those can be None, 

303 # and hence defer checking if that's what the user originally wanted. 

304 return DataCoordinateSet( 

305 frozenset(self._dataIds), 

306 graph=self._graph, 

307 hasFull=self._hasFull, 

308 hasRecords=self._hasRecords, 

309 check=False, 

310 ) 

311 

312 def toSequence(self) -> DataCoordinateSequence: 

313 # Docstring inherited from DataCoordinateIterable. 

314 # Override base class to pass in attributes instead of results of 

315 # method calls for _hasFull and _hasRecords - those can be None, 

316 # and hence defer checking if that's what the user originally wanted. 

317 return DataCoordinateSequence( 

318 tuple(self._dataIds), 

319 graph=self._graph, 

320 hasFull=self._hasFull, 

321 hasRecords=self._hasRecords, 

322 check=False, 

323 ) 

324 

325 def __iter__(self) -> Iterator[DataCoordinate]: 

326 return iter(self._dataIds) 

327 

328 def __len__(self) -> int: 

329 return len(self._dataIds) 

330 

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

332 key = DataCoordinate.standardize(key, universe=self.universe) 

333 return key in self._dataIds 

334 

335 def _subsetKwargs(self, graph: DimensionGraph) -> Dict[str, Any]: 

336 """Return constructor kwargs useful for subclasses implementing subset. 

337 

338 Parameters 

339 ---------- 

340 graph : `DimensionGraph` 

341 Dimensions passed to `subset`. 

342 

343 Returns 

344 ------- 

345 **kwargs 

346 A dict with `hasFull`, `hasRecords`, and `check` keys, associated 

347 with the appropriate values for a `subset` operation with the given 

348 dimensions. 

349 """ 

350 hasFull: Optional[bool] 

351 if graph.dimensions <= self.graph.required: 

352 hasFull = True 

353 else: 

354 hasFull = self._hasFull 

355 return dict(hasFull=hasFull, hasRecords=self._hasRecords, check=False) 

356 

357 

358class DataCoordinateSet(_DataCoordinateCollectionBase): 

359 """Iterable iteration that is set-like. 

360 

361 A `DataCoordinateIterable` implementation that adds some set-like 

362 functionality, and is backed by a true set-like object. 

363 

364 Parameters 

365 ---------- 

366 dataIds : `collections.abc.Set` [ `DataCoordinate` ] 

367 A set of `DataCoordinate` instances, with dimensions equal to 

368 ``graph``. If this is a mutable object, the caller must be able to 

369 guarantee that it will not be modified by any other holders. 

370 graph : `DimensionGraph` 

371 Dimensions identified by all data IDs in the set. 

372 hasFull : `bool`, optional 

373 If `True`, the caller guarantees that `DataCoordinate.hasFull` returns 

374 `True` for all given data IDs. If `False`, no such guarantee is made, 

375 and `DataCoordinateSet.hasFull` will always return `False`. If `None` 

376 (default), `DataCoordinateSet.hasFull` will be computed from the given 

377 data IDs, immediately if ``check`` is `True`, or on first use if 

378 ``check`` is `False`. 

379 hasRecords : `bool`, optional 

380 If `True`, the caller guarantees that `DataCoordinate.hasRecords` 

381 returns `True` for all given data IDs. If `False`, no such guarantee 

382 is made and `DataCoordinateSet.hasRecords` will always return `False`. 

383 If `None` (default), `DataCoordinateSet.hasRecords` will be computed 

384 from the given data IDs, immediately if ``check`` is `True`, or on 

385 first use if ``check`` is `False`. 

386 check: `bool`, optional 

387 If `True` (default) check that all data IDs are consistent with the 

388 given ``graph`` and state flags at construction. If `False`, no 

389 checking will occur. 

390 

391 Notes 

392 ----- 

393 `DataCoordinateSet` does not formally implement the `collections.abc.Set` 

394 interface, because that requires many binary operations to accept any 

395 set-like object as the other argument (regardless of what its elements 

396 might be), and it's much easier to ensure those operations never behave 

397 surprisingly if we restrict them to `DataCoordinateSet` or (sometimes) 

398 `DataCoordinateIterable`, and in most cases restrict that they identify 

399 the same dimensions. In particular: 

400 

401 - a `DataCoordinateSet` will compare as not equal to any object that is 

402 not a `DataCoordinateSet`, even native Python sets containing the exact 

403 same elements; 

404 

405 - subset/superset comparison _operators_ (``<``, ``>``, ``<=``, ``>=``) 

406 require both operands to be `DataCoordinateSet` instances that have the 

407 same dimensions (i.e. ``graph`` attribute); 

408 

409 - `issubset`, `issuperset`, and `isdisjoint` require the other argument to 

410 be a `DataCoordinateIterable` with the same dimensions; 

411 

412 - operators that create new sets (``&``, ``|``, ``^``, ``-``) require both 

413 operands to be `DataCoordinateSet` instances that have the same 

414 dimensions _and_ the same ``dtype``; 

415 

416 - named methods that create new sets (`intersection`, `union`, 

417 `symmetric_difference`, `difference`) require the other operand to be a 

418 `DataCoordinateIterable` with the same dimensions _and_ the same 

419 ``dtype``. 

420 

421 In addition, when the two operands differ in the return values of `hasFull` 

422 and/or `hasRecords`, we make no guarantees about what those methods will 

423 return on the new `DataCoordinateSet` (other than that they will accurately 

424 reflect what elements are in the new set - we just don't control which 

425 elements are contributed by each operand). 

426 """ 

427 

428 def __init__( 

429 self, 

430 dataIds: AbstractSet[DataCoordinate], 

431 graph: DimensionGraph, 

432 *, 

433 hasFull: Optional[bool] = None, 

434 hasRecords: Optional[bool] = None, 

435 check: bool = True, 

436 ): 

437 super().__init__(dataIds, graph, hasFull=hasFull, hasRecords=hasRecords, check=check) 

438 

439 _dataIds: AbstractSet[DataCoordinate] 

440 

441 __slots__ = () 

442 

443 def __str__(self) -> str: 

444 return str(set(self._dataIds)) 

445 

446 def __repr__(self) -> str: 

447 return ( 

448 f"DataCoordinateSet({set(self._dataIds)}, {self._graph!r}, " 

449 f"hasFull={self._hasFull}, hasRecords={self._hasRecords})" 

450 ) 

451 

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

453 if isinstance(other, DataCoordinateSet): 

454 return self._graph == other._graph and self._dataIds == other._dataIds 

455 return False 

456 

457 def __le__(self, other: DataCoordinateSet) -> bool: 

458 if self.graph != other.graph: 

459 raise ValueError(f"Inconsistent dimensions in set comparision: {self.graph} != {other.graph}.") 

460 return self._dataIds <= other._dataIds 

461 

462 def __ge__(self, other: DataCoordinateSet) -> bool: 

463 if self.graph != other.graph: 

464 raise ValueError(f"Inconsistent dimensions in set comparision: {self.graph} != {other.graph}.") 

465 return self._dataIds >= other._dataIds 

466 

467 def __lt__(self, other: DataCoordinateSet) -> bool: 

468 if self.graph != other.graph: 

469 raise ValueError(f"Inconsistent dimensions in set comparision: {self.graph} != {other.graph}.") 

470 return self._dataIds < other._dataIds 

471 

472 def __gt__(self, other: DataCoordinateSet) -> bool: 

473 if self.graph != other.graph: 

474 raise ValueError(f"Inconsistent dimensions in set comparision: {self.graph} != {other.graph}.") 

475 return self._dataIds > other._dataIds 

476 

477 def issubset(self, other: DataCoordinateIterable) -> bool: 

478 """Test whether ``self`` contains all data IDs in ``other``. 

479 

480 Parameters 

481 ---------- 

482 other : `DataCoordinateIterable` 

483 An iterable of data IDs with ``other.graph == self.graph``. 

484 

485 Returns 

486 ------- 

487 issubset : `bool` 

488 `True` if all data IDs in ``self`` are also in ``other``, and 

489 `False` otherwise. 

490 """ 

491 if self.graph != other.graph: 

492 raise ValueError(f"Inconsistent dimensions in set comparision: {self.graph} != {other.graph}.") 

493 return self._dataIds <= other.toSet()._dataIds 

494 

495 def issuperset(self, other: DataCoordinateIterable) -> bool: 

496 """Test whether ``other`` contains all data IDs in ``self``. 

497 

498 Parameters 

499 ---------- 

500 other : `DataCoordinateIterable` 

501 An iterable of data IDs with ``other.graph == self.graph``. 

502 

503 Returns 

504 ------- 

505 issuperset : `bool` 

506 `True` if all data IDs in ``other`` are also in ``self``, and 

507 `False` otherwise. 

508 """ 

509 if self.graph != other.graph: 

510 raise ValueError(f"Inconsistent dimensions in set comparision: {self.graph} != {other.graph}.") 

511 return self._dataIds >= other.toSet()._dataIds 

512 

513 def isdisjoint(self, other: DataCoordinateIterable) -> bool: 

514 """Test whether there are no data IDs in both ``self`` and ``other``. 

515 

516 Parameters 

517 ---------- 

518 other : `DataCoordinateIterable` 

519 An iterable of data IDs with ``other.graph == self.graph``. 

520 

521 Returns 

522 ------- 

523 isdisjoint : `bool` 

524 `True` if there are no data IDs in both ``self`` and ``other``, and 

525 `False` otherwise. 

526 """ 

527 if self.graph != other.graph: 

528 raise ValueError(f"Inconsistent dimensions in set comparision: {self.graph} != {other.graph}.") 

529 return self._dataIds.isdisjoint(other.toSet()._dataIds) 

530 

531 def __and__(self, other: DataCoordinateSet) -> DataCoordinateSet: 

532 if self.graph != other.graph: 

533 raise ValueError(f"Inconsistent dimensions in set operation: {self.graph} != {other.graph}.") 

534 return DataCoordinateSet(self._dataIds & other._dataIds, self.graph, check=False) 

535 

536 def __or__(self, other: DataCoordinateSet) -> DataCoordinateSet: 

537 if self.graph != other.graph: 

538 raise ValueError(f"Inconsistent dimensions in set operation: {self.graph} != {other.graph}.") 

539 return DataCoordinateSet(self._dataIds | other._dataIds, self.graph, check=False) 

540 

541 def __xor__(self, other: DataCoordinateSet) -> DataCoordinateSet: 

542 if self.graph != other.graph: 

543 raise ValueError(f"Inconsistent dimensions in set operation: {self.graph} != {other.graph}.") 

544 return DataCoordinateSet(self._dataIds ^ other._dataIds, self.graph, check=False) 

545 

546 def __sub__(self, other: DataCoordinateSet) -> DataCoordinateSet: 

547 if self.graph != other.graph: 

548 raise ValueError(f"Inconsistent dimensions in set operation: {self.graph} != {other.graph}.") 

549 return DataCoordinateSet(self._dataIds - other._dataIds, self.graph, check=False) 

550 

551 def intersection(self, other: DataCoordinateIterable) -> DataCoordinateSet: 

552 """Return a new set that contains all data IDs from parameters. 

553 

554 Parameters 

555 ---------- 

556 other : `DataCoordinateIterable` 

557 An iterable of data IDs with ``other.graph == self.graph``. 

558 

559 Returns 

560 ------- 

561 intersection : `DataCoordinateSet` 

562 A new `DataCoordinateSet` instance. 

563 """ 

564 if self.graph != other.graph: 

565 raise ValueError(f"Inconsistent dimensions in set operation: {self.graph} != {other.graph}.") 

566 return DataCoordinateSet(self._dataIds & other.toSet()._dataIds, self.graph, check=False) 

567 

568 def union(self, other: DataCoordinateIterable) -> DataCoordinateSet: 

569 """Return a new set that contains all data IDs in either parameters. 

570 

571 Parameters 

572 ---------- 

573 other : `DataCoordinateIterable` 

574 An iterable of data IDs with ``other.graph == self.graph``. 

575 

576 Returns 

577 ------- 

578 intersection : `DataCoordinateSet` 

579 A new `DataCoordinateSet` instance. 

580 """ 

581 if self.graph != other.graph: 

582 raise ValueError(f"Inconsistent dimensions in set operation: {self.graph} != {other.graph}.") 

583 return DataCoordinateSet(self._dataIds | other.toSet()._dataIds, self.graph, check=False) 

584 

585 def symmetric_difference(self, other: DataCoordinateIterable) -> DataCoordinateSet: 

586 """Return a new set with all data IDs in either parameters, not both. 

587 

588 Parameters 

589 ---------- 

590 other : `DataCoordinateIterable` 

591 An iterable of data IDs with ``other.graph == self.graph``. 

592 

593 Returns 

594 ------- 

595 intersection : `DataCoordinateSet` 

596 A new `DataCoordinateSet` instance. 

597 """ 

598 if self.graph != other.graph: 

599 raise ValueError(f"Inconsistent dimensions in set operation: {self.graph} != {other.graph}.") 

600 return DataCoordinateSet(self._dataIds ^ other.toSet()._dataIds, self.graph, check=False) 

601 

602 def difference(self, other: DataCoordinateIterable) -> DataCoordinateSet: 

603 """Return a new set with all data IDs in this that are not in other. 

604 

605 Parameters 

606 ---------- 

607 other : `DataCoordinateIterable` 

608 An iterable of data IDs with ``other.graph == self.graph``. 

609 

610 Returns 

611 ------- 

612 intersection : `DataCoordinateSet` 

613 A new `DataCoordinateSet` instance. 

614 """ 

615 if self.graph != other.graph: 

616 raise ValueError(f"Inconsistent dimensions in set operation: {self.graph} != {other.graph}.") 

617 return DataCoordinateSet(self._dataIds - other.toSet()._dataIds, self.graph, check=False) 

618 

619 def toSet(self) -> DataCoordinateSet: 

620 # Docstring inherited from DataCoordinateIterable. 

621 return self 

622 

623 def subset(self, graph: DimensionGraph) -> DataCoordinateSet: 

624 """Return a set whose data IDs identify a subset. 

625 

626 Parameters 

627 ---------- 

628 graph : `DimensionGraph` 

629 Dimensions to be identified by the data IDs in the returned 

630 iterable. Must be a subset of ``self.graph``. 

631 

632 Returns 

633 ------- 

634 set : `DataCoordinateSet` 

635 A `DataCoordinateSet` with ``set.graph == graph``. 

636 Will be ``self`` if ``graph == self.graph``. Elements are 

637 equivalent to those that would be created by calling 

638 `DataCoordinate.subset` on all elements in ``self``, with 

639 deduplication but and in arbitrary order. 

640 """ 

641 if graph == self.graph: 

642 return self 

643 return DataCoordinateSet( 

644 {dataId.subset(graph) for dataId in self._dataIds}, graph, **self._subsetKwargs(graph) 

645 ) 

646 

647 

648class DataCoordinateSequence(_DataCoordinateCollectionBase, Sequence[DataCoordinate]): 

649 """Iterable supporting the full Sequence interface. 

650 

651 A `DataCoordinateIterable` implementation that supports the full 

652 `collections.abc.Sequence` interface. 

653 

654 Parameters 

655 ---------- 

656 dataIds : `collections.abc.Sequence` [ `DataCoordinate` ] 

657 A sequence of `DataCoordinate` instances, with dimensions equal to 

658 ``graph``. 

659 graph : `DimensionGraph` 

660 Dimensions identified by all data IDs in the set. 

661 hasFull : `bool`, optional 

662 If `True`, the caller guarantees that `DataCoordinate.hasFull` returns 

663 `True` for all given data IDs. If `False`, no such guarantee is made, 

664 and `DataCoordinateSet.hasFull` will always return `False`. If `None` 

665 (default), `DataCoordinateSet.hasFull` will be computed from the given 

666 data IDs, immediately if ``check`` is `True`, or on first use if 

667 ``check`` is `False`. 

668 hasRecords : `bool`, optional 

669 If `True`, the caller guarantees that `DataCoordinate.hasRecords` 

670 returns `True` for all given data IDs. If `False`, no such guarantee 

671 is made and `DataCoordinateSet.hasRecords` will always return `False`. 

672 If `None` (default), `DataCoordinateSet.hasRecords` will be computed 

673 from the given data IDs, immediately if ``check`` is `True`, or on 

674 first use if ``check`` is `False`. 

675 check: `bool`, optional 

676 If `True` (default) check that all data IDs are consistent with the 

677 given ``graph`` and state flags at construction. If `False`, no 

678 checking will occur. 

679 """ 

680 

681 def __init__( 

682 self, 

683 dataIds: Sequence[DataCoordinate], 

684 graph: DimensionGraph, 

685 *, 

686 hasFull: Optional[bool] = None, 

687 hasRecords: Optional[bool] = None, 

688 check: bool = True, 

689 ): 

690 super().__init__(tuple(dataIds), graph, hasFull=hasFull, hasRecords=hasRecords, check=check) 

691 

692 _dataIds: Sequence[DataCoordinate] 

693 

694 __slots__ = () 

695 

696 def __str__(self) -> str: 

697 return str(tuple(self._dataIds)) 

698 

699 def __repr__(self) -> str: 

700 return ( 

701 f"DataCoordinateSequence({tuple(self._dataIds)}, {self._graph!r}, " 

702 f"hasFull={self._hasFull}, hasRecords={self._hasRecords})" 

703 ) 

704 

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

706 if isinstance(other, DataCoordinateSequence): 

707 return self._graph == other._graph and self._dataIds == other._dataIds 

708 return False 

709 

710 @overload 

711 def __getitem__(self, index: int) -> DataCoordinate: 

712 pass 

713 

714 @overload # noqa: F811 (FIXME: remove for py 3.8+) 

715 def __getitem__(self, index: slice) -> DataCoordinateSequence: # noqa: F811 

716 pass 

717 

718 def __getitem__(self, index: Any) -> Any: # noqa: F811 

719 r = self._dataIds[index] 

720 if isinstance(index, slice): 

721 return DataCoordinateSequence( 

722 r, self._graph, hasFull=self._hasFull, hasRecords=self._hasRecords, check=False 

723 ) 

724 return r 

725 

726 def toSequence(self) -> DataCoordinateSequence: 

727 # Docstring inherited from DataCoordinateIterable. 

728 return self 

729 

730 def subset(self, graph: DimensionGraph) -> DataCoordinateSequence: 

731 """Return a sequence whose data IDs identify a subset. 

732 

733 Parameters 

734 ---------- 

735 graph : `DimensionGraph` 

736 Dimensions to be identified by the data IDs in the returned 

737 iterable. Must be a subset of ``self.graph``. 

738 

739 Returns 

740 ------- 

741 set : `DataCoordinateSequence` 

742 A `DataCoordinateSequence` with ``set.graph == graph``. 

743 Will be ``self`` if ``graph == self.graph``. Elements are 

744 equivalent to those that would be created by calling 

745 `DataCoordinate.subset` on all elements in ``self``, in the same 

746 order and with no deduplication. 

747 """ 

748 if graph == self.graph: 

749 return self 

750 return DataCoordinateSequence( 

751 tuple(dataId.subset(graph) for dataId in self._dataIds), graph, **self._subsetKwargs(graph) 

752 )