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

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/>.
22from __future__ import annotations
24__all__ = (
25 "DataCoordinateIterable",
26 "DataCoordinateSet",
27 "DataCoordinateSequence",
28)
30from abc import abstractmethod
31from typing import (
32 AbstractSet,
33 Any,
34 Callable,
35 Collection,
36 Dict,
37 Iterable,
38 Iterator,
39 List,
40 Optional,
41 overload,
42 Sequence,
43)
45import sqlalchemy
47from ..simpleQuery import SimpleQuery
48from ._coordinate import DataCoordinate
49from ._graph import DimensionGraph
50from ._universe import DimensionUniverse
53class DataCoordinateIterable(Iterable[DataCoordinate]):
54 """An abstract base class for homogeneous iterables of data IDs.
56 All elements of a `DataCoordinateIterable` identify the same set of
57 dimensions (given by the `graph` property) and generally have the same
58 `DataCoordinate.hasFull` and `DataCoordinate.hasRecords` flag values.
59 """
61 __slots__ = ()
63 @staticmethod
64 def fromScalar(dataId: DataCoordinate) -> _ScalarDataCoordinateIterable:
65 """Return a `DataCoordinateIterable` containing the single data ID
66 given.
68 Parameters
69 ----------
70 dataId : `DataCoordinate`
71 Data ID to adapt. Must be a true `DataCoordinate` instance, not
72 an arbitrary mapping. No runtime checking is performed.
74 Returns
75 -------
76 iterable : `DataCoordinateIterable`
77 A `DataCoordinateIterable` instance of unspecified (i.e.
78 implementation-detail) subclass. Guaranteed to implement
79 the `collections.abc.Sized` (i.e. `__len__`) and
80 `collections.abc.Container` (i.e. `__contains__`) interfaces as
81 well as that of `DataCoordinateIterable`.
82 """
83 return _ScalarDataCoordinateIterable(dataId)
85 @property
86 @abstractmethod
87 def graph(self) -> DimensionGraph:
88 """The dimensions identified by these data IDs (`DimensionGraph`).
89 """
90 raise NotImplementedError()
92 @property
93 def universe(self) -> DimensionUniverse:
94 """The universe that defines all known dimensions compatible with
95 this iterable (`DimensionUniverse`).
96 """
97 return self.graph.universe
99 @abstractmethod
100 def hasFull(self) -> bool:
101 """Return whether all data IDs in this iterable identify all
102 dimensions, not just required dimensions.
104 Returns
105 -------
106 state : `bool`
107 If `True`, ``all(d.hasFull() for d in iterable)`` is guaranteed.
108 If `False`, no guarantees are made.
109 """
110 raise NotImplementedError()
112 @abstractmethod
113 def hasRecords(self) -> bool:
114 """Return whether all data IDs in this iterable contain
115 `DimensionRecord` instances.
117 Returns
118 -------
119 state : `bool`
120 If `True`, ``all(d.hasRecords() for d in iterable)`` is guaranteed.
121 If `False`, no guarantees are made.
122 """
123 raise NotImplementedError()
125 def toSet(self) -> DataCoordinateSet:
126 """Transform this iterable into a `DataCoordinateSet`.
128 Returns
129 -------
130 set : `DataCoordinateSet`
131 A `DatasetCoordinateSet` instance with the same elements as
132 ``self``, after removing any duplicates. May be ``self`` if it is
133 already a `DataCoordinateSet`.
134 """
135 return DataCoordinateSet(frozenset(self), graph=self.graph,
136 hasFull=self.hasFull(),
137 hasRecords=self.hasRecords(),
138 check=False)
140 def toSequence(self) -> DataCoordinateSequence:
141 """Transform this iterable into a `DataCoordinateSequence`.
143 Returns
144 -------
145 seq : `DataCoordinateSequence`
146 A new `DatasetCoordinateSequence` with the same elements as
147 ``self``, in the same order. May be ``self`` if it is already a
148 `DataCoordinateSequence`.
149 """
150 return DataCoordinateSequence(tuple(self), graph=self.graph,
151 hasFull=self.hasFull(),
152 hasRecords=self.hasRecords(),
153 check=False)
155 def constrain(self, query: SimpleQuery, columns: Callable[[str], sqlalchemy.sql.ColumnElement]) -> None:
156 """Constrain a SQL query to include or relate to only data IDs in
157 this iterable.
159 Parameters
160 ----------
161 query : `SimpleQuery`
162 Struct that represents the SQL query to constrain, either by
163 appending to its WHERE clause, joining a new table or subquery,
164 or both.
165 columns : `Callable`
166 A callable that accepts `str` dimension names and returns
167 SQLAlchemy objects representing a column for that dimension's
168 primary key value in the query.
169 """
170 toOrTogether: List[sqlalchemy.sql.ColumnElement] = []
171 for dataId in self:
172 toOrTogether.append(
173 sqlalchemy.sql.and_(*[
174 columns(dimension.name) == dataId[dimension.name]
175 for dimension in self.graph.required
176 ])
177 )
178 query.where.append(sqlalchemy.sql.or_(*toOrTogether))
180 @abstractmethod
181 def subset(self, graph: DimensionGraph) -> DataCoordinateIterable:
182 """Return an iterable whose data IDs identify a subset of the
183 dimensions that this one's do.
185 Parameters
186 ----------
187 graph : `DimensionGraph`
188 Dimensions to be identified by the data IDs in the returned
189 iterable. Must be a subset of ``self.graph``.
191 Returns
192 -------
193 iterable : `DataCoordinateIterable`
194 A `DataCoordinateIterable` with ``iterable.graph == graph``.
195 May be ``self`` if ``graph == self.graph``. Elements are
196 equivalent to those that would be created by calling
197 `DataCoordinate.subset` on all elements in ``self``, possibly
198 with deduplication and/or reordeding (depending on the subclass,
199 which may make more specific guarantees).
200 """
201 raise NotImplementedError()
204class _ScalarDataCoordinateIterable(DataCoordinateIterable):
205 """A `DataCoordinateIterable` implementation that adapts a single
206 `DataCoordinate` instance.
208 This class should only be used directly by other code in the module in
209 which it is defined; all other code should interact with it only through
210 the `DataCoordinateIterable` interface.
212 Parameters
213 ----------
214 dataId : `DataCoordinate`
215 The data ID to adapt.
216 """
217 def __init__(self, dataId: DataCoordinate):
218 self._dataId = dataId
220 __slots__ = ("_dataId",)
222 def __iter__(self) -> Iterator[DataCoordinate]:
223 yield self._dataId
225 def __len__(self) -> int:
226 return 1
228 def __contains__(self, key: Any) -> bool:
229 if isinstance(key, DataCoordinate):
230 return key == self._dataId
231 else:
232 return False
234 @property
235 def graph(self) -> DimensionGraph:
236 # Docstring inherited from DataCoordinateIterable.
237 return self._dataId.graph
239 def hasFull(self) -> bool:
240 # Docstring inherited from DataCoordinateIterable.
241 return self._dataId.hasFull()
243 def hasRecords(self) -> bool:
244 # Docstring inherited from DataCoordinateIterable.
245 return self._dataId.hasRecords()
247 def subset(self, graph: DimensionGraph) -> _ScalarDataCoordinateIterable:
248 # Docstring inherited from DataCoordinateIterable.
249 return _ScalarDataCoordinateIterable(self._dataId.subset(graph))
252class _DataCoordinateCollectionBase(DataCoordinateIterable):
253 """A partial `DataCoordinateIterable` implementation that is backed by a
254 native Python collection.
256 This class is intended only to be used as an intermediate base class for
257 `DataCoordinateIterables` that assume a more specific type of collection
258 and can hence make more informed choices for how to implement some methods.
260 Parameters
261 ----------
262 dataIds : `collections.abc.Collection` [ `DataCoordinate` ]
263 A collection of `DataCoordinate` instances, with dimensions equal to
264 ``graph``.
265 graph : `DimensionGraph`
266 Dimensions identified by all data IDs in the set.
267 hasFull : `bool`, optional
268 If `True`, the caller guarantees that `DataCoordinate.hasFull` returns
269 `True` for all given data IDs. If `False`, no such guarantee is made,
270 and `hasFull` will always return `False`. If `None` (default),
271 `hasFull` will be computed from the given data IDs, immediately if
272 ``check`` is `True`, or on first use if ``check`` is `False`.
273 hasRecords : `bool`, optional
274 If `True`, the caller guarantees that `DataCoordinate.hasRecords`
275 returns `True` for all given data IDs. If `False`, no such guarantee
276 is made and `hasRecords` will always return `False`. If `None`
277 (default), `hasRecords` will be computed from the given data IDs,
278 immediately if ``check`` is `True`, or on first use if ``check`` is
279 `False`.
280 check: `bool`, optional
281 If `True` (default) check that all data IDs are consistent with the
282 given ``graph`` and state flags at construction. If `False`, no
283 checking will occur.
284 """
285 def __init__(self, dataIds: Collection[DataCoordinate], graph: DimensionGraph, *,
286 hasFull: Optional[bool] = None, hasRecords: Optional[bool] = None,
287 check: bool = True):
288 self._dataIds = dataIds
289 self._graph = graph
290 if check:
291 for dataId in self._dataIds:
292 if hasFull and not dataId.hasFull():
293 raise ValueError(f"{dataId} is not complete, but is required to be.")
294 if hasRecords and not dataId.hasRecords():
295 raise ValueError(f"{dataId} has no records, but is required to.")
296 if dataId.graph != self._graph:
297 raise ValueError(f"Bad dimensions {dataId.graph}; expected {self._graph}.")
298 if hasFull is None:
299 hasFull = all(dataId.hasFull() for dataId in self._dataIds)
300 if hasRecords is None:
301 hasRecords = all(dataId.hasRecords() for dataId in self._dataIds)
302 self._hasFull = hasFull
303 self._hasRecords = hasRecords
305 __slots__ = ("_graph", "_dataIds", "_hasFull", "_hasRecords")
307 @property
308 def graph(self) -> DimensionGraph:
309 # Docstring inherited from DataCoordinateIterable.
310 return self._graph
312 def hasFull(self) -> bool:
313 # Docstring inherited from DataCoordinateIterable.
314 if self._hasFull is None:
315 self._hasFull = all(dataId.hasFull() for dataId in self._dataIds)
316 return self._hasFull
318 def hasRecords(self) -> bool:
319 # Docstring inherited from DataCoordinateIterable.
320 if self._hasRecords is None:
321 self._hasRecords = all(dataId.hasRecords() for dataId in self._dataIds)
322 return self._hasRecords
324 def toSet(self) -> DataCoordinateSet:
325 # Docstring inherited from DataCoordinateIterable.
326 # Override base class to pass in attributes instead of results of
327 # method calls for _hasFull and _hasRecords - those can be None,
328 # and hence defer checking if that's what the user originally wanted.
329 return DataCoordinateSet(frozenset(self._dataIds), graph=self._graph,
330 hasFull=self._hasFull,
331 hasRecords=self._hasRecords,
332 check=False)
334 def toSequence(self) -> DataCoordinateSequence:
335 # Docstring inherited from DataCoordinateIterable.
336 # Override base class to pass in attributes instead of results of
337 # method calls for _hasFull and _hasRecords - those can be None,
338 # and hence defer checking if that's what the user originally wanted.
339 return DataCoordinateSequence(tuple(self._dataIds), graph=self._graph,
340 hasFull=self._hasFull,
341 hasRecords=self._hasRecords,
342 check=False)
344 def __iter__(self) -> Iterator[DataCoordinate]:
345 return iter(self._dataIds)
347 def __len__(self) -> int:
348 return len(self._dataIds)
350 def __contains__(self, key: Any) -> bool:
351 key = DataCoordinate.standardize(key, universe=self.universe)
352 return key in self._dataIds
354 def _subsetKwargs(self, graph: DimensionGraph) -> Dict[str, Any]:
355 """Return constructor keyword arguments useful for subclasses
356 implementing `subset`.
358 Parameters
359 ----------
360 graph : `DimensionGraph`
361 Dimensions passed to `subset`.
363 Returns
364 -------
365 kwargs : `dict`
366 A dict with `hasFull`, `hasRecords`, and `check` keys, associated
367 with the appropriate values for a `subset` operation with the given
368 dimensions.
369 """
370 hasFull: Optional[bool]
371 if graph.dimensions <= self.graph.required:
372 hasFull = True
373 else:
374 hasFull = self._hasFull
375 return dict(hasFull=hasFull, hasRecords=self._hasRecords, check=False)
378class DataCoordinateSet(_DataCoordinateCollectionBase):
379 """A `DataCoordinateIterable` implementation that adds some set-like
380 functionality, and is backed by a true set-like object.
382 Parameters
383 ----------
384 dataIds : `collections.abc.Set` [ `DataCoordinate` ]
385 A set of `DataCoordinate` instances, with dimensions equal to
386 ``graph``. If this is a mutable object, the caller must be able to
387 guarantee that it will not be modified by any other holders.
388 graph : `DimensionGraph`
389 Dimensions identified by all data IDs in the set.
390 hasFull : `bool`, optional
391 If `True`, the caller guarantees that `DataCoordinate.hasFull` returns
392 `True` for all given data IDs. If `False`, no such guarantee is made,
393 and `DataCoordinateSet.hasFull` will always return `False`. If `None`
394 (default), `DataCoordinateSet.hasFull` will be computed from the given
395 data IDs, immediately if ``check`` is `True`, or on first use if
396 ``check`` is `False`.
397 hasRecords : `bool`, optional
398 If `True`, the caller guarantees that `DataCoordinate.hasRecords`
399 returns `True` for all given data IDs. If `False`, no such guarantee
400 is made and `DataCoordinateSet.hasRecords` will always return `False`.
401 If `None` (default), `DataCoordinateSet.hasRecords` will be computed
402 from the given data IDs, immediately if ``check`` is `True`, or on
403 first use if ``check`` is `False`.
404 check: `bool`, optional
405 If `True` (default) check that all data IDs are consistent with the
406 given ``graph`` and state flags at construction. If `False`, no
407 checking will occur.
409 Notes
410 -----
411 `DataCoordinateSet` does not formally implement the `collections.abc.Set`
412 interface, because that requires many binary operations to accept any
413 set-like object as the other argument (regardless of what its elements
414 might be), and it's much easier to ensure those operations never behave
415 surprisingly if we restrict them to `DataCoordinateSet` or (sometimes)
416 `DataCoordinateIterable`, and in most cases restrict that they identify
417 the same dimensions. In particular:
419 - a `DataCoordinateSet` will compare as not equal to any object that is
420 not a `DataCoordinateSet`, even native Python sets containing the exact
421 same elements;
423 - subset/superset comparison _operators_ (``<``, ``>``, ``<=``, ``>=``)
424 require both operands to be `DataCoordinateSet` instances that have the
425 same dimensions (i.e. ``graph`` attribute);
427 - `issubset`, `issuperset`, and `isdisjoint` require the other argument to
428 be a `DataCoordinateIterable` with the same dimensions;
430 - operators that create new sets (``&``, ``|``, ``^``, ``-``) require both
431 operands to be `DataCoordinateSet` instances that have the same
432 dimensions _and_ the same ``dtype``;
434 - named methods that create new sets (`intersection`, `union`,
435 `symmetric_difference`, `difference`) require the other operand to be a
436 `DataCoordinateIterable` with the same dimensions _and_ the same
437 ``dtype``.
439 In addition, when the two operands differ in the return values of `hasFull`
440 and/or `hasRecords`, we make no guarantees about what those methods will
441 return on the new `DataCoordinateSet` (other than that they will accurately
442 reflect what elements are in the new set - we just don't control which
443 elements are contributed by each operand).
444 """
445 def __init__(self, dataIds: AbstractSet[DataCoordinate], graph: DimensionGraph, *,
446 hasFull: Optional[bool] = None, hasRecords: Optional[bool] = None,
447 check: bool = True):
448 super().__init__(dataIds, graph, hasFull=hasFull, hasRecords=hasRecords, check=check)
450 _dataIds: AbstractSet[DataCoordinate]
452 __slots__ = ()
454 def __str__(self) -> str:
455 return str(set(self._dataIds))
457 def __repr__(self) -> str:
458 return (f"DataCoordinateSet({set(self._dataIds)}, {self._graph!r}, "
459 f"hasFull={self._hasFull}, hasRecords={self._hasRecords})")
461 def __eq__(self, other: Any) -> bool:
462 if isinstance(other, DataCoordinateSet):
463 return (
464 self._graph == other._graph
465 and self._dataIds == other._dataIds
466 )
467 return False
469 def __le__(self, other: DataCoordinateSet) -> bool:
470 if self.graph != other.graph:
471 raise ValueError(f"Inconsistent dimensions in set comparision: {self.graph} != {other.graph}.")
472 return self._dataIds <= other._dataIds
474 def __ge__(self, other: DataCoordinateSet) -> bool:
475 if self.graph != other.graph:
476 raise ValueError(f"Inconsistent dimensions in set comparision: {self.graph} != {other.graph}.")
477 return self._dataIds >= other._dataIds
479 def __lt__(self, other: DataCoordinateSet) -> bool:
480 if self.graph != other.graph:
481 raise ValueError(f"Inconsistent dimensions in set comparision: {self.graph} != {other.graph}.")
482 return self._dataIds < other._dataIds
484 def __gt__(self, other: DataCoordinateSet) -> bool:
485 if self.graph != other.graph:
486 raise ValueError(f"Inconsistent dimensions in set comparision: {self.graph} != {other.graph}.")
487 return self._dataIds > other._dataIds
489 def issubset(self, other: DataCoordinateIterable) -> bool:
490 """Test whether ``self`` contains all data IDs in ``other``.
492 Parameters
493 ----------
494 other : `DataCoordinateIterable`
495 An iterable of data IDs with ``other.graph == self.graph``.
497 Returns
498 -------
499 issubset : `bool`
500 `True` if all data IDs in ``self`` are also in ``other``, and
501 `False` otherwise.
502 """
503 if self.graph != other.graph:
504 raise ValueError(f"Inconsistent dimensions in set comparision: {self.graph} != {other.graph}.")
505 return self._dataIds <= other.toSet()._dataIds
507 def issuperset(self, other: DataCoordinateIterable) -> bool:
508 """Test whether ``other`` contains all data IDs in ``self``.
510 Parameters
511 ----------
512 other : `DataCoordinateIterable`
513 An iterable of data IDs with ``other.graph == self.graph``.
515 Returns
516 -------
517 issuperset : `bool`
518 `True` if all data IDs in ``other`` are also in ``self``, and
519 `False` otherwise.
520 """
521 if self.graph != other.graph:
522 raise ValueError(f"Inconsistent dimensions in set comparision: {self.graph} != {other.graph}.")
523 return self._dataIds >= other.toSet()._dataIds
525 def isdisjoint(self, other: DataCoordinateIterable) -> bool:
526 """Test whether there are no data IDs in both ``self`` and ``other``.
528 Parameters
529 ----------
530 other : `DataCoordinateIterable`
531 An iterable of data IDs with ``other.graph == self.graph``.
533 Returns
534 -------
535 isdisjoint : `bool`
536 `True` if there are no data IDs in both ``self`` and ``other``, and
537 `False` otherwise.
538 """
539 if self.graph != other.graph:
540 raise ValueError(f"Inconsistent dimensions in set comparision: {self.graph} != {other.graph}.")
541 return self._dataIds.isdisjoint(other.toSet()._dataIds)
543 def __and__(self, other: DataCoordinateSet) -> DataCoordinateSet:
544 if self.graph != other.graph:
545 raise ValueError(f"Inconsistent dimensions in set operation: {self.graph} != {other.graph}.")
546 return DataCoordinateSet(self._dataIds & other._dataIds, self.graph, check=False)
548 def __or__(self, other: DataCoordinateSet) -> DataCoordinateSet:
549 if self.graph != other.graph:
550 raise ValueError(f"Inconsistent dimensions in set operation: {self.graph} != {other.graph}.")
551 return DataCoordinateSet(self._dataIds | other._dataIds, self.graph, check=False)
553 def __xor__(self, other: DataCoordinateSet) -> DataCoordinateSet:
554 if self.graph != other.graph:
555 raise ValueError(f"Inconsistent dimensions in set operation: {self.graph} != {other.graph}.")
556 return DataCoordinateSet(self._dataIds ^ other._dataIds, self.graph, check=False)
558 def __sub__(self, other: DataCoordinateSet) -> DataCoordinateSet:
559 if self.graph != other.graph:
560 raise ValueError(f"Inconsistent dimensions in set operation: {self.graph} != {other.graph}.")
561 return DataCoordinateSet(self._dataIds - other._dataIds, self.graph, check=False)
563 def intersection(self, other: DataCoordinateIterable) -> DataCoordinateSet:
564 """Return a new set that contains all data IDs in both ``self`` and
565 ``other``.
567 Parameters
568 ----------
569 other : `DataCoordinateIterable`
570 An iterable of data IDs with ``other.graph == self.graph``.
572 Returns
573 -------
574 intersection : `DataCoordinateSet`
575 A new `DataCoordinateSet` instance.
576 """
577 if self.graph != other.graph:
578 raise ValueError(f"Inconsistent dimensions in set operation: {self.graph} != {other.graph}.")
579 return DataCoordinateSet(self._dataIds & other.toSet()._dataIds, self.graph, check=False)
581 def union(self, other: DataCoordinateIterable) -> DataCoordinateSet:
582 """Return a new set that contains all data IDs in either ``self`` or
583 ``other``.
585 Parameters
586 ----------
587 other : `DataCoordinateIterable`
588 An iterable of data IDs with ``other.graph == self.graph``.
590 Returns
591 -------
592 intersection : `DataCoordinateSet`
593 A new `DataCoordinateSet` instance.
594 """
595 if self.graph != other.graph:
596 raise ValueError(f"Inconsistent dimensions in set operation: {self.graph} != {other.graph}.")
597 return DataCoordinateSet(self._dataIds | other.toSet()._dataIds, self.graph, check=False)
599 def symmetric_difference(self, other: DataCoordinateIterable) -> DataCoordinateSet:
600 """Return a new set that contains all data IDs in either ``self`` or
601 ``other``, but not both.
603 Parameters
604 ----------
605 other : `DataCoordinateIterable`
606 An iterable of data IDs with ``other.graph == self.graph``.
608 Returns
609 -------
610 intersection : `DataCoordinateSet`
611 A new `DataCoordinateSet` instance.
612 """
613 if self.graph != other.graph:
614 raise ValueError(f"Inconsistent dimensions in set operation: {self.graph} != {other.graph}.")
615 return DataCoordinateSet(self._dataIds ^ other.toSet()._dataIds, self.graph, check=False)
617 def difference(self, other: DataCoordinateIterable) -> DataCoordinateSet:
618 """Return a new set that contains all data IDs in ``self`` that are not
619 in ``other``.
621 Parameters
622 ----------
623 other : `DataCoordinateIterable`
624 An iterable of data IDs with ``other.graph == self.graph``.
626 Returns
627 -------
628 intersection : `DataCoordinateSet`
629 A new `DataCoordinateSet` instance.
630 """
631 if self.graph != other.graph:
632 raise ValueError(f"Inconsistent dimensions in set operation: {self.graph} != {other.graph}.")
633 return DataCoordinateSet(self._dataIds - other.toSet()._dataIds, self.graph, check=False)
635 def toSet(self) -> DataCoordinateSet:
636 # Docstring inherited from DataCoordinateIterable.
637 return self
639 def subset(self, graph: DimensionGraph) -> DataCoordinateSet:
640 """Return a set whose data IDs identify a subset of the
641 dimensions that this one's do.
643 Parameters
644 ----------
645 graph : `DimensionGraph`
646 Dimensions to be identified by the data IDs in the returned
647 iterable. Must be a subset of ``self.graph``.
649 Returns
650 -------
651 set : `DataCoordinateSet`
652 A `DataCoordinateSet` with ``set.graph == graph``.
653 Will be ``self`` if ``graph == self.graph``. Elements are
654 equivalent to those that would be created by calling
655 `DataCoordinate.subset` on all elements in ``self``, with
656 deduplication but and in arbitrary order.
657 """
658 if graph == self.graph:
659 return self
660 return DataCoordinateSet(
661 {dataId.subset(graph) for dataId in self._dataIds},
662 graph,
663 **self._subsetKwargs(graph)
664 )
667class DataCoordinateSequence(_DataCoordinateCollectionBase, Sequence[DataCoordinate]):
668 """A `DataCoordinateIterable` implementation that supports the full
669 `collections.abc.Sequence` interface.
671 Parameters
672 ----------
673 dataIds : `collections.abc.Sequence` [ `DataCoordinate` ]
674 A sequence of `DataCoordinate` instances, with dimensions equal to
675 ``graph``.
676 graph : `DimensionGraph`
677 Dimensions identified by all data IDs in the set.
678 hasFull : `bool`, optional
679 If `True`, the caller guarantees that `DataCoordinate.hasFull` returns
680 `True` for all given data IDs. If `False`, no such guarantee is made,
681 and `DataCoordinateSet.hasFull` will always return `False`. If `None`
682 (default), `DataCoordinateSet.hasFull` will be computed from the given
683 data IDs, immediately if ``check`` is `True`, or on first use if
684 ``check`` is `False`.
685 hasRecords : `bool`, optional
686 If `True`, the caller guarantees that `DataCoordinate.hasRecords`
687 returns `True` for all given data IDs. If `False`, no such guarantee
688 is made and `DataCoordinateSet.hasRecords` will always return `False`.
689 If `None` (default), `DataCoordinateSet.hasRecords` will be computed
690 from the given data IDs, immediately if ``check`` is `True`, or on
691 first use if ``check`` is `False`.
692 check: `bool`, optional
693 If `True` (default) check that all data IDs are consistent with the
694 given ``graph`` and state flags at construction. If `False`, no
695 checking will occur.
696 """
697 def __init__(self, dataIds: Sequence[DataCoordinate], graph: DimensionGraph, *,
698 hasFull: Optional[bool] = None, hasRecords: Optional[bool] = None,
699 check: bool = True):
700 super().__init__(tuple(dataIds), graph, hasFull=hasFull, hasRecords=hasRecords, check=check)
702 _dataIds: Sequence[DataCoordinate]
704 __slots__ = ()
706 def __str__(self) -> str:
707 return str(tuple(self._dataIds))
709 def __repr__(self) -> str:
710 return (f"DataCoordinateSequence({tuple(self._dataIds)}, {self._graph!r}, "
711 f"hasFull={self._hasFull}, hasRecords={self._hasRecords})")
713 def __eq__(self, other: Any) -> bool:
714 if isinstance(other, DataCoordinateSequence):
715 return (
716 self._graph == other._graph
717 and self._dataIds == other._dataIds
718 )
719 return False
721 @overload
722 def __getitem__(self, index: int) -> DataCoordinate:
723 pass
725 @overload # noqa: F811 (FIXME: remove for py 3.8+)
726 def __getitem__(self, index: slice) -> DataCoordinateSequence: # noqa: F811
727 pass
729 def __getitem__(self, index: Any) -> Any: # noqa: F811
730 r = self._dataIds[index]
731 if isinstance(index, slice):
732 return DataCoordinateSequence(r, self._graph,
733 hasFull=self._hasFull, hasRecords=self._hasRecords,
734 check=False)
735 return r
737 def toSequence(self) -> DataCoordinateSequence:
738 # Docstring inherited from DataCoordinateIterable.
739 return self
741 def subset(self, graph: DimensionGraph) -> DataCoordinateSequence:
742 """Return a sequence whose data IDs identify a subset of the
743 dimensions that this one's do.
745 Parameters
746 ----------
747 graph : `DimensionGraph`
748 Dimensions to be identified by the data IDs in the returned
749 iterable. Must be a subset of ``self.graph``.
751 Returns
752 -------
753 set : `DataCoordinateSequence`
754 A `DataCoordinateSequence` with ``set.graph == graph``.
755 Will be ``self`` if ``graph == self.graph``. Elements are
756 equivalent to those that would be created by calling
757 `DataCoordinate.subset` on all elements in ``self``, in the same
758 order and with no deduplication.
759 """
760 if graph == self.graph:
761 return self
762 return DataCoordinateSequence(
763 tuple(dataId.subset(graph) for dataId in self._dataIds),
764 graph,
765 **self._subsetKwargs(graph)
766 )