Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions src/contraqctor/contract/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -605,18 +605,27 @@ def __str__(self: Self) -> str:
def __iter__(self) -> Generator[DataStream, None, None]:
"""Iterator for child data streams.

Yields:
DataStream: Child data streams.
A collection that failed to load has no valid children to traverse, so it
yields nothing rather than re-raising its stored ``ErrorOnLoad``. This keeps
traversal consistent with ``load_all(strict=False)``: a partially-loaded
hierarchy stays iterable, and load errors remain retrievable via
:meth:`collect_errors`. Accessing a stream's ``.data`` directly still raises.

Yields:
DataStream: Child data streams (none if this collection failed to load).
"""
if self.has_error:
return
# We intentionally yield from self.data to trigger
# automatic loading if needed
yield from self.data

def iter_all(self) -> Generator[DataStream, None, None]:
"""Iterator for all child data streams, including nested collections.

Implements a depth-first traversal of the stream hierarchy.
Implements a depth-first traversal of the stream hierarchy. An errored
sub-collection is yielded as a node but not descended into (see
:meth:`__iter__`), so traversal never aborts on a partially-loaded tree.

Yields:
DataStream: All recursively yielded child data streams.
Expand Down
43 changes: 43 additions & 0 deletions tests/test_contract/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -502,3 +502,46 @@ def _reader(params: SimpleParams) -> list:
assert collection.has_error
with pytest.raises(ValueError, match="Data must be a list of DataStreams."):
collection.collect_errors()[0].raise_from_error()

def test_iter_failed_collection_yields_nothing(self, temp_dir):
"""Iterating a collection that failed to load yields nothing instead of re-raising."""
nonexistent_path = temp_dir / "nonexistent_folder" / "device.yml"
failing_collection = _FailingCollection(name="failing", reader_params=SimpleParams(path=nonexistent_path))

failing_collection.load()
assert failing_collection.has_error

# Iteration / traversal must not re-raise the stored ErrorOnLoad.
assert list(failing_collection) == []
assert list(failing_collection.iter_all()) == []

# Accessing the data directly still raises.
with pytest.raises(FileNotFoundError):
_ = failing_collection.data

def test_iter_all_survives_failed_subcollection(self, text_file, temp_dir):
"""iter_all traverses a partial hierarchy without aborting on an errored sub-collection."""
working = SimpleDataStream(name="working", reader_params=SimpleParams(path=text_file))

nonexistent_path = temp_dir / "nonexistent_folder" / "device.yml"
failing_collection = _FailingCollection(name="failing", reader_params=SimpleParams(path=nonexistent_path))

root = DataStreamCollection(name="root", data_streams=[working, failing_collection])
root.load_all(strict=False)
assert failing_collection.has_error

# Traversal must not raise; it yields the streams that loaded plus the errored node,
# but nothing from inside the errored sub-collection.
all_streams = list(root.iter_all())
assert working in all_streams
assert failing_collection in all_streams

# Direct iteration of the parent also works.
children = list(root)
assert working in children
assert failing_collection in children

# Errors are still surfaced through the normal error-collection path.
errors = root.collect_errors()
assert len(errors) == 1
assert errors[0].data_stream == failing_collection