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.
67 Parameters
68 ----------
69 dataId : `DataCoordinate`
70 Data ID to adapt. Must be a true `DataCoordinate` instance, not
71 an arbitrary mapping. No runtime checking is performed.
73 Returns
74 -------
75 iterable : `DataCoordinateIterable`
76 A `DataCoordinateIterable` instance of unspecified (i.e.
77 implementation-detail) subclass. Guaranteed to implement
78 the `collections.abc.Sized` (i.e. `__len__`) and
79 `collections.abc.Container` (i.e. `__contains__`) interfaces as
80 well as that of `DataCoordinateIterable`.
81 """
82 return _ScalarDataCoordinateIterable(dataId)
84 @property
85 @abstractmethod
86 def graph(self) -> DimensionGraph:
87 """Dimensions identified by these data IDs (`DimensionGraph`)."""
88 raise NotImplementedError()
90 @property
91 def universe(self) -> DimensionUniverse:
92 """Universe that defines all known compatible dimensions.
94 (`DimensionUniverse`).
95 """
96 return self.graph.universe
98 @abstractmethod
99 def hasFull(self) -> bool:
100 """Indicate if all data IDs in this iterable identify all dimensions.
102 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 records.
116 Returns
117 -------
118 state : `bool`
119 If `True`, ``all(d.hasRecords() for d in iterable)`` is guaranteed.
120 If `False`, no guarantees are made.
121 """
122 raise NotImplementedError()
124 def toSet(self) -> DataCoordinateSet:
125 """Transform this iterable into a `DataCoordinateSet`.
127 Returns
128 -------
129 set : `DataCoordinateSet`
130 A `DatasetCoordinateSet` instance with the same elements as
131 ``self``, after removing any duplicates. May be ``self`` if it is
132 already a `DataCoordinateSet`.
133 """
134 return DataCoordinateSet(frozenset(self), graph=self.graph,
135 hasFull=self.hasFull(),
136 hasRecords=self.hasRecords(),
137 check=False)
139 def toSequence(self) -> DataCoordinateSequence:
140 """Transform this iterable into a `DataCoordinateSequence`.
142 Returns
143 -------
144 seq : `DataCoordinateSequence`
145 A new `DatasetCoordinateSequence` with the same elements as
146 ``self``, in the same order. May be ``self`` if it is already a
147 `DataCoordinateSequence`.
148 """
149 return DataCoordinateSequence(tuple(self), graph=self.graph,
150 hasFull=self.hasFull(),
151 hasRecords=self.hasRecords(),
152 check=False)
154 def constrain(self, query: SimpleQuery, columns: Callable[[str], sqlalchemy.sql.ColumnElement]) -> None:
155 """Constrain a SQL query to include or relate to only known data IDs.
157 Parameters
158 ----------
159 query : `SimpleQuery`
160 Struct that represents the SQL query to constrain, either by
161 appending to its WHERE clause, joining a new table or subquery,
162 or both.
163 columns : `Callable`
164 A callable that accepts `str` dimension names and returns
165 SQLAlchemy objects representing a column for that dimension's
166 primary key value in the query.
167 """
168 toOrTogether: List[sqlalchemy.sql.ColumnElement] = []
169 for dataId in self:
170 toOrTogether.append(
171 sqlalchemy.sql.and_(*[
172 columns(dimension.name) == dataId[dimension.name]
173 for dimension in self.graph.required
174 ])
175 )
176 query.where.append(sqlalchemy.sql.or_(*toOrTogether))
178 @abstractmethod
179 def subset(self, graph: DimensionGraph) -> DataCoordinateIterable:
180 """Return a subset iterable.
182 This subset iterable returns data IDs that 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 """An iterable for a single `DataCoordinate`.
207 A `DataCoordinateIterable` implementation that adapts a single
208 `DataCoordinate` instance.
210 This class should only be used directly by other code in the module in
211 which it is defined; all other code should interact with it only through
212 the `DataCoordinateIterable` interface.
214 Parameters
215 ----------
216 dataId : `DataCoordinate`
217 The data ID to adapt.
218 """
220 def __init__(self, dataId: DataCoordinate):
221 self._dataId = dataId
223 __slots__ = ("_dataId",)
225 def __iter__(self) -> Iterator[DataCoordinate]:
226 yield self._dataId
228 def __len__(self) -> int:
229 return 1
231 def __contains__(self, key: Any) -> bool:
232 if isinstance(key, DataCoordinate):
233 return key == self._dataId
234 else:
235 return False
237 @property
238 def graph(self) -> DimensionGraph:
239 # Docstring inherited from DataCoordinateIterable.
240 return self._dataId.graph
242 def hasFull(self) -> bool:
243 # Docstring inherited from DataCoordinateIterable.
244 return self._dataId.hasFull()
246 def hasRecords(self) -> bool:
247 # Docstring inherited from DataCoordinateIterable.
248 return self._dataId.hasRecords()
250 def subset(self, graph: DimensionGraph) -> _ScalarDataCoordinateIterable:
251 # Docstring inherited from DataCoordinateIterable.
252 return _ScalarDataCoordinateIterable(self._dataId.subset(graph))
255class _DataCoordinateCollectionBase(DataCoordinateIterable):
256 """A partial iterable implementation backed by native Python collection.
258 A partial `DataCoordinateIterable` implementation that is backed by a
259 native Python collection.
261 This class is intended only to be used as an intermediate base class for
262 `DataCoordinateIterables` that assume a more specific type of collection
263 and can hence make more informed choices for how to implement some methods.
265 Parameters
266 ----------
267 dataIds : `collections.abc.Collection` [ `DataCoordinate` ]
268 A collection of `DataCoordinate` instances, with dimensions equal to
269 ``graph``.
270 graph : `DimensionGraph`
271 Dimensions identified by all data IDs in the set.
272 hasFull : `bool`, optional
273 If `True`, the caller guarantees that `DataCoordinate.hasFull` returns
274 `True` for all given data IDs. If `False`, no such guarantee is made,
275 and `hasFull` will always return `False`. If `None` (default),
276 `hasFull` will be computed from the given data IDs, immediately if
277 ``check`` is `True`, or on first use if ``check`` is `False`.
278 hasRecords : `bool`, optional
279 If `True`, the caller guarantees that `DataCoordinate.hasRecords`
280 returns `True` for all given data IDs. If `False`, no such guarantee
281 is made and `hasRecords` will always return `False`. If `None`
282 (default), `hasRecords` will be computed from the given data IDs,
283 immediately if ``check`` is `True`, or on first use if ``check`` is
284 `False`.
285 check: `bool`, optional
286 If `True` (default) check that all data IDs are consistent with the
287 given ``graph`` and state flags at construction. If `False`, no
288 checking will occur.
289 """
291 def __init__(self, dataIds: Collection[DataCoordinate], graph: DimensionGraph, *,
292 hasFull: Optional[bool] = None, hasRecords: Optional[bool] = None,
293 check: bool = True):
294 self._dataIds = dataIds
295 self._graph = graph
296 if check:
297 for dataId in self._dataIds:
298 if hasFull and not dataId.hasFull():
299 raise ValueError(f"{dataId} is not complete, but is required to be.")
300 if hasRecords and not dataId.hasRecords():
301 raise ValueError(f"{dataId} has no records, but is required to.")
302 if dataId.graph != self._graph:
303 raise ValueError(f"Bad dimensions {dataId.graph}; expected {self._graph}.")
304 if hasFull is None:
305 hasFull = all(dataId.hasFull() for dataId in self._dataIds)
306 if hasRecords is None:
307 hasRecords = all(dataId.hasRecords() for dataId in self._dataIds)
308 self._hasFull = hasFull
309 self._hasRecords = hasRecords
311 __slots__ = ("_graph", "_dataIds", "_hasFull", "_hasRecords")
313 @property
314 def graph(self) -> DimensionGraph:
315 # Docstring inherited from DataCoordinateIterable.
316 return self._graph
318 def hasFull(self) -> bool:
319 # Docstring inherited from DataCoordinateIterable.
320 if self._hasFull is None:
321 self._hasFull = all(dataId.hasFull() for dataId in self._dataIds)
322 return self._hasFull
324 def hasRecords(self) -> bool:
325 # Docstring inherited from DataCoordinateIterable.
326 if self._hasRecords is None:
327 self._hasRecords = all(dataId.hasRecords() for dataId in self._dataIds)
328 return self._hasRecords
330 def toSet(self) -> DataCoordinateSet:
331 # Docstring inherited from DataCoordinateIterable.
332 # Override base class to pass in attributes instead of results of
333 # method calls for _hasFull and _hasRecords - those can be None,
334 # and hence defer checking if that's what the user originally wanted.
335 return DataCoordinateSet(frozenset(self._dataIds), graph=self._graph,
336 hasFull=self._hasFull,
337 hasRecords=self._hasRecords,
338 check=False)
340 def toSequence(self) -> DataCoordinateSequence:
341 # Docstring inherited from DataCoordinateIterable.
342 # Override base class to pass in attributes instead of results of
343 # method calls for _hasFull and _hasRecords - those can be None,
344 # and hence defer checking if that's what the user originally wanted.
345 return DataCoordinateSequence(tuple(self._dataIds), graph=self._graph,
346 hasFull=self._hasFull,
347 hasRecords=self._hasRecords,
348 check=False)
350 def __iter__(self) -> Iterator[DataCoordinate]:
351 return iter(self._dataIds)
353 def __len__(self) -> int:
354 return len(self._dataIds)
356 def __contains__(self, key: Any) -> bool:
357 key = DataCoordinate.standardize(key, universe=self.universe)
358 return key in self._dataIds
360 def _subsetKwargs(self, graph: DimensionGraph) -> Dict[str, Any]:
361 """Return constructor kwargs useful for subclasses implementing subset.
363 Parameters
364 ----------
365 graph : `DimensionGraph`
366 Dimensions passed to `subset`.
368 Returns
369 -------
370 **kwargs
371 A dict with `hasFull`, `hasRecords`, and `check` keys, associated
372 with the appropriate values for a `subset` operation with the given
373 dimensions.
374 """
375 hasFull: Optional[bool]
376 if graph.dimensions <= self.graph.required:
377 hasFull = True
378 else:
379 hasFull = self._hasFull
380 return dict(hasFull=hasFull, hasRecords=self._hasRecords, check=False)
383class DataCoordinateSet(_DataCoordinateCollectionBase):
384 """Iterable iteration that is set-like.
386 A `DataCoordinateIterable` implementation that adds some set-like
387 functionality, and is backed by a true set-like object.
389 Parameters
390 ----------
391 dataIds : `collections.abc.Set` [ `DataCoordinate` ]
392 A set of `DataCoordinate` instances, with dimensions equal to
393 ``graph``. If this is a mutable object, the caller must be able to
394 guarantee that it will not be modified by any other holders.
395 graph : `DimensionGraph`
396 Dimensions identified by all data IDs in the set.
397 hasFull : `bool`, optional
398 If `True`, the caller guarantees that `DataCoordinate.hasFull` returns
399 `True` for all given data IDs. If `False`, no such guarantee is made,
400 and `DataCoordinateSet.hasFull` will always return `False`. If `None`
401 (default), `DataCoordinateSet.hasFull` will be computed from the given
402 data IDs, immediately if ``check`` is `True`, or on first use if
403 ``check`` is `False`.
404 hasRecords : `bool`, optional
405 If `True`, the caller guarantees that `DataCoordinate.hasRecords`
406 returns `True` for all given data IDs. If `False`, no such guarantee
407 is made and `DataCoordinateSet.hasRecords` will always return `False`.
408 If `None` (default), `DataCoordinateSet.hasRecords` will be computed
409 from the given data IDs, immediately if ``check`` is `True`, or on
410 first use if ``check`` is `False`.
411 check: `bool`, optional
412 If `True` (default) check that all data IDs are consistent with the
413 given ``graph`` and state flags at construction. If `False`, no
414 checking will occur.
416 Notes
417 -----
418 `DataCoordinateSet` does not formally implement the `collections.abc.Set`
419 interface, because that requires many binary operations to accept any
420 set-like object as the other argument (regardless of what its elements
421 might be), and it's much easier to ensure those operations never behave
422 surprisingly if we restrict them to `DataCoordinateSet` or (sometimes)
423 `DataCoordinateIterable`, and in most cases restrict that they identify
424 the same dimensions. In particular:
426 - a `DataCoordinateSet` will compare as not equal to any object that is
427 not a `DataCoordinateSet`, even native Python sets containing the exact
428 same elements;
430 - subset/superset comparison _operators_ (``<``, ``>``, ``<=``, ``>=``)
431 require both operands to be `DataCoordinateSet` instances that have the
432 same dimensions (i.e. ``graph`` attribute);
434 - `issubset`, `issuperset`, and `isdisjoint` require the other argument to
435 be a `DataCoordinateIterable` with the same dimensions;
437 - operators that create new sets (``&``, ``|``, ``^``, ``-``) require both
438 operands to be `DataCoordinateSet` instances that have the same
439 dimensions _and_ the same ``dtype``;
441 - named methods that create new sets (`intersection`, `union`,
442 `symmetric_difference`, `difference`) require the other operand to be a
443 `DataCoordinateIterable` with the same dimensions _and_ the same
444 ``dtype``.
446 In addition, when the two operands differ in the return values of `hasFull`
447 and/or `hasRecords`, we make no guarantees about what those methods will
448 return on the new `DataCoordinateSet` (other than that they will accurately
449 reflect what elements are in the new set - we just don't control which
450 elements are contributed by each operand).
451 """
453 def __init__(self, dataIds: AbstractSet[DataCoordinate], graph: DimensionGraph, *,
454 hasFull: Optional[bool] = None, hasRecords: Optional[bool] = None,
455 check: bool = True):
456 super().__init__(dataIds, graph, hasFull=hasFull, hasRecords=hasRecords, check=check)
458 _dataIds: AbstractSet[DataCoordinate]
460 __slots__ = ()
462 def __str__(self) -> str:
463 return str(set(self._dataIds))
465 def __repr__(self) -> str:
466 return (f"DataCoordinateSet({set(self._dataIds)}, {self._graph!r}, "
467 f"hasFull={self._hasFull}, hasRecords={self._hasRecords})")
469 def __eq__(self, other: Any) -> bool:
470 if isinstance(other, DataCoordinateSet):
471 return (
472 self._graph == other._graph
473 and self._dataIds == other._dataIds
474 )
475 return False
477 def __le__(self, other: DataCoordinateSet) -> bool:
478 if self.graph != other.graph:
479 raise ValueError(f"Inconsistent dimensions in set comparision: {self.graph} != {other.graph}.")
480 return self._dataIds <= other._dataIds
482 def __ge__(self, other: DataCoordinateSet) -> bool:
483 if self.graph != other.graph:
484 raise ValueError(f"Inconsistent dimensions in set comparision: {self.graph} != {other.graph}.")
485 return self._dataIds >= other._dataIds
487 def __lt__(self, other: DataCoordinateSet) -> bool:
488 if self.graph != other.graph:
489 raise ValueError(f"Inconsistent dimensions in set comparision: {self.graph} != {other.graph}.")
490 return self._dataIds < other._dataIds
492 def __gt__(self, other: DataCoordinateSet) -> bool:
493 if self.graph != other.graph:
494 raise ValueError(f"Inconsistent dimensions in set comparision: {self.graph} != {other.graph}.")
495 return self._dataIds > other._dataIds
497 def issubset(self, other: DataCoordinateIterable) -> bool:
498 """Test whether ``self`` contains all data IDs in ``other``.
500 Parameters
501 ----------
502 other : `DataCoordinateIterable`
503 An iterable of data IDs with ``other.graph == self.graph``.
505 Returns
506 -------
507 issubset : `bool`
508 `True` if all data IDs in ``self`` are also in ``other``, and
509 `False` otherwise.
510 """
511 if self.graph != other.graph:
512 raise ValueError(f"Inconsistent dimensions in set comparision: {self.graph} != {other.graph}.")
513 return self._dataIds <= other.toSet()._dataIds
515 def issuperset(self, other: DataCoordinateIterable) -> bool:
516 """Test whether ``other`` contains all data IDs in ``self``.
518 Parameters
519 ----------
520 other : `DataCoordinateIterable`
521 An iterable of data IDs with ``other.graph == self.graph``.
523 Returns
524 -------
525 issuperset : `bool`
526 `True` if all data IDs in ``other`` are also in ``self``, and
527 `False` otherwise.
528 """
529 if self.graph != other.graph:
530 raise ValueError(f"Inconsistent dimensions in set comparision: {self.graph} != {other.graph}.")
531 return self._dataIds >= other.toSet()._dataIds
533 def isdisjoint(self, other: DataCoordinateIterable) -> bool:
534 """Test whether there are no data IDs in both ``self`` and ``other``.
536 Parameters
537 ----------
538 other : `DataCoordinateIterable`
539 An iterable of data IDs with ``other.graph == self.graph``.
541 Returns
542 -------
543 isdisjoint : `bool`
544 `True` if there are no data IDs in both ``self`` and ``other``, and
545 `False` otherwise.
546 """
547 if self.graph != other.graph:
548 raise ValueError(f"Inconsistent dimensions in set comparision: {self.graph} != {other.graph}.")
549 return self._dataIds.isdisjoint(other.toSet()._dataIds)
551 def __and__(self, other: DataCoordinateSet) -> DataCoordinateSet:
552 if self.graph != other.graph:
553 raise ValueError(f"Inconsistent dimensions in set operation: {self.graph} != {other.graph}.")
554 return DataCoordinateSet(self._dataIds & other._dataIds, self.graph, check=False)
556 def __or__(self, other: DataCoordinateSet) -> DataCoordinateSet:
557 if self.graph != other.graph:
558 raise ValueError(f"Inconsistent dimensions in set operation: {self.graph} != {other.graph}.")
559 return DataCoordinateSet(self._dataIds | other._dataIds, self.graph, check=False)
561 def __xor__(self, other: DataCoordinateSet) -> DataCoordinateSet:
562 if self.graph != other.graph:
563 raise ValueError(f"Inconsistent dimensions in set operation: {self.graph} != {other.graph}.")
564 return DataCoordinateSet(self._dataIds ^ other._dataIds, self.graph, check=False)
566 def __sub__(self, other: DataCoordinateSet) -> DataCoordinateSet:
567 if self.graph != other.graph:
568 raise ValueError(f"Inconsistent dimensions in set operation: {self.graph} != {other.graph}.")
569 return DataCoordinateSet(self._dataIds - other._dataIds, self.graph, check=False)
571 def intersection(self, other: DataCoordinateIterable) -> DataCoordinateSet:
572 """Return a new set that contains all data IDs from parameters.
574 Parameters
575 ----------
576 other : `DataCoordinateIterable`
577 An iterable of data IDs with ``other.graph == self.graph``.
579 Returns
580 -------
581 intersection : `DataCoordinateSet`
582 A new `DataCoordinateSet` instance.
583 """
584 if self.graph != other.graph:
585 raise ValueError(f"Inconsistent dimensions in set operation: {self.graph} != {other.graph}.")
586 return DataCoordinateSet(self._dataIds & other.toSet()._dataIds, self.graph, check=False)
588 def union(self, other: DataCoordinateIterable) -> DataCoordinateSet:
589 """Return a new set that contains all data IDs in either parameters.
591 Parameters
592 ----------
593 other : `DataCoordinateIterable`
594 An iterable of data IDs with ``other.graph == self.graph``.
596 Returns
597 -------
598 intersection : `DataCoordinateSet`
599 A new `DataCoordinateSet` instance.
600 """
601 if self.graph != other.graph:
602 raise ValueError(f"Inconsistent dimensions in set operation: {self.graph} != {other.graph}.")
603 return DataCoordinateSet(self._dataIds | other.toSet()._dataIds, self.graph, check=False)
605 def symmetric_difference(self, other: DataCoordinateIterable) -> DataCoordinateSet:
606 """Return a new set with all data IDs in either parameters, not both.
608 Parameters
609 ----------
610 other : `DataCoordinateIterable`
611 An iterable of data IDs with ``other.graph == self.graph``.
613 Returns
614 -------
615 intersection : `DataCoordinateSet`
616 A new `DataCoordinateSet` instance.
617 """
618 if self.graph != other.graph:
619 raise ValueError(f"Inconsistent dimensions in set operation: {self.graph} != {other.graph}.")
620 return DataCoordinateSet(self._dataIds ^ other.toSet()._dataIds, self.graph, check=False)
622 def difference(self, other: DataCoordinateIterable) -> DataCoordinateSet:
623 """Return a new set with all data IDs in this that are not in other.
625 Parameters
626 ----------
627 other : `DataCoordinateIterable`
628 An iterable of data IDs with ``other.graph == self.graph``.
630 Returns
631 -------
632 intersection : `DataCoordinateSet`
633 A new `DataCoordinateSet` instance.
634 """
635 if self.graph != other.graph:
636 raise ValueError(f"Inconsistent dimensions in set operation: {self.graph} != {other.graph}.")
637 return DataCoordinateSet(self._dataIds - other.toSet()._dataIds, self.graph, check=False)
639 def toSet(self) -> DataCoordinateSet:
640 # Docstring inherited from DataCoordinateIterable.
641 return self
643 def subset(self, graph: DimensionGraph) -> DataCoordinateSet:
644 """Return a set whose data IDs identify a subset.
646 Parameters
647 ----------
648 graph : `DimensionGraph`
649 Dimensions to be identified by the data IDs in the returned
650 iterable. Must be a subset of ``self.graph``.
652 Returns
653 -------
654 set : `DataCoordinateSet`
655 A `DataCoordinateSet` with ``set.graph == graph``.
656 Will be ``self`` if ``graph == self.graph``. Elements are
657 equivalent to those that would be created by calling
658 `DataCoordinate.subset` on all elements in ``self``, with
659 deduplication but and in arbitrary order.
660 """
661 if graph == self.graph:
662 return self
663 return DataCoordinateSet(
664 {dataId.subset(graph) for dataId in self._dataIds},
665 graph,
666 **self._subsetKwargs(graph)
667 )
670class DataCoordinateSequence(_DataCoordinateCollectionBase, Sequence[DataCoordinate]):
671 """Iterable supporting the full Sequence interface.
673 A `DataCoordinateIterable` implementation that supports the full
674 `collections.abc.Sequence` interface.
676 Parameters
677 ----------
678 dataIds : `collections.abc.Sequence` [ `DataCoordinate` ]
679 A sequence of `DataCoordinate` instances, with dimensions equal to
680 ``graph``.
681 graph : `DimensionGraph`
682 Dimensions identified by all data IDs in the set.
683 hasFull : `bool`, optional
684 If `True`, the caller guarantees that `DataCoordinate.hasFull` returns
685 `True` for all given data IDs. If `False`, no such guarantee is made,
686 and `DataCoordinateSet.hasFull` will always return `False`. If `None`
687 (default), `DataCoordinateSet.hasFull` will be computed from the given
688 data IDs, immediately if ``check`` is `True`, or on first use if
689 ``check`` is `False`.
690 hasRecords : `bool`, optional
691 If `True`, the caller guarantees that `DataCoordinate.hasRecords`
692 returns `True` for all given data IDs. If `False`, no such guarantee
693 is made and `DataCoordinateSet.hasRecords` will always return `False`.
694 If `None` (default), `DataCoordinateSet.hasRecords` will be computed
695 from the given data IDs, immediately if ``check`` is `True`, or on
696 first use if ``check`` is `False`.
697 check: `bool`, optional
698 If `True` (default) check that all data IDs are consistent with the
699 given ``graph`` and state flags at construction. If `False`, no
700 checking will occur.
701 """
703 def __init__(self, dataIds: Sequence[DataCoordinate], graph: DimensionGraph, *,
704 hasFull: Optional[bool] = None, hasRecords: Optional[bool] = None,
705 check: bool = True):
706 super().__init__(tuple(dataIds), graph, hasFull=hasFull, hasRecords=hasRecords, check=check)
708 _dataIds: Sequence[DataCoordinate]
710 __slots__ = ()
712 def __str__(self) -> str:
713 return str(tuple(self._dataIds))
715 def __repr__(self) -> str:
716 return (f"DataCoordinateSequence({tuple(self._dataIds)}, {self._graph!r}, "
717 f"hasFull={self._hasFull}, hasRecords={self._hasRecords})")
719 def __eq__(self, other: Any) -> bool:
720 if isinstance(other, DataCoordinateSequence):
721 return (
722 self._graph == other._graph
723 and self._dataIds == other._dataIds
724 )
725 return False
727 @overload
728 def __getitem__(self, index: int) -> DataCoordinate:
729 pass
731 @overload # noqa: F811 (FIXME: remove for py 3.8+)
732 def __getitem__(self, index: slice) -> DataCoordinateSequence: # noqa: F811
733 pass
735 def __getitem__(self, index: Any) -> Any: # noqa: F811
736 r = self._dataIds[index]
737 if isinstance(index, slice):
738 return DataCoordinateSequence(r, self._graph,
739 hasFull=self._hasFull, hasRecords=self._hasRecords,
740 check=False)
741 return r
743 def toSequence(self) -> DataCoordinateSequence:
744 # Docstring inherited from DataCoordinateIterable.
745 return self
747 def subset(self, graph: DimensionGraph) -> DataCoordinateSequence:
748 """Return a sequence whose data IDs identify a subset.
750 Parameters
751 ----------
752 graph : `DimensionGraph`
753 Dimensions to be identified by the data IDs in the returned
754 iterable. Must be a subset of ``self.graph``.
756 Returns
757 -------
758 set : `DataCoordinateSequence`
759 A `DataCoordinateSequence` with ``set.graph == graph``.
760 Will be ``self`` if ``graph == self.graph``. Elements are
761 equivalent to those that would be created by calling
762 `DataCoordinate.subset` on all elements in ``self``, in the same
763 order and with no deduplication.
764 """
765 if graph == self.graph:
766 return self
767 return DataCoordinateSequence(
768 tuple(dataId.subset(graph) for dataId in self._dataIds),
769 graph,
770 **self._subsetKwargs(graph)
771 )