Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DM-44583: support manual config outputs in mocking system #420

Merged
merged 5 commits into from
May 30, 2024
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
12 changes: 4 additions & 8 deletions .github/workflows/build_docs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -25,25 +25,21 @@ jobs:
- name: Install graphviz
run: sudo apt-get install graphviz

- name: Set the VIRTUAL_ENV variable for uv to work
run: |
echo "VIRTUAL_ENV=${Python_ROOT_DIR}" >> $GITHUB_ENV

- name: Update pip/wheel infrastructure and install uv
run: |
python -m pip install --upgrade pip
pip install uv
uv pip install wheel
uv pip install --system wheel

- name: Install documenteer
run: uv pip install 'documenteer[pipelines]==0.8.2'
run: uv pip install --system 'documenteer[pipelines]==0.8.2'

- name: Install dependencies
run: |
uv pip install -r requirements.txt
uv pip install --system -r requirements.txt

- name: Build and install
run: uv pip install --no-deps -v -e .
run: uv pip install --system --no-deps -v -e .

- name: Build documentation
working-directory: ./doc
Expand Down
1 change: 1 addition & 0 deletions doc/changes/DM-44583.feature.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add mocking support for tasks that write regular datasets with config, log, or metadata storage classes.
52 changes: 27 additions & 25 deletions python/lsst/pipe/base/tests/mocks/_pipeline_task.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,14 +50,19 @@
from lsst.utils.introspection import get_full_type_name
from lsst.utils.iteration import ensure_iterable

from ... import automatic_connection_constants as acc
from ... import connectionTypes as cT
from ...config import PipelineTaskConfig
from ...connections import InputQuantizedConnection, OutputQuantizedConnection, PipelineTaskConnections
from ...pipeline_graph import PipelineGraph
from ...pipelineTask import PipelineTask
from ._data_id_match import DataIdMatch
from ._storage_class import MockDataset, MockDatasetQuantum, MockStorageClass, get_mock_name
from ._storage_class import (
ConvertedUnmockedDataset,
MockDataset,
MockDatasetQuantum,
MockStorageClass,
get_mock_name,
)

_LOG = logging.getLogger(__name__)

Expand Down Expand Up @@ -107,7 +112,8 @@
Original tasks and configuration to mock.
unmocked_dataset_types : `~collections.abc.Iterable` [ `str` ], optional
Names of overall-input dataset types that should not be replaced with
mocks.
mocks. "Automatic" datasets written by the execution framework such
as configs, logs, and metadata are implicitly included.
force_failures : `~collections.abc.Mapping` [ `str`, `ForcedFailure` ]
Mapping from original task label to information about an exception one
or more quanta for this task should raise.
Expand All @@ -118,10 +124,15 @@
Pipeline graph using `MockPipelineTask` configurations that target the
original tasks. Never resolved.
"""
unmocked_dataset_types = tuple(unmocked_dataset_types)
unmocked_dataset_types = list(unmocked_dataset_types)

Check warning on line 127 in python/lsst/pipe/base/tests/mocks/_pipeline_task.py

View check run for this annotation

Codecov / codecov/patch

python/lsst/pipe/base/tests/mocks/_pipeline_task.py#L127

Added line #L127 was not covered by tests
if force_failures is None:
force_failures = {}
result = PipelineGraph(description=original_graph.description)
for task_node in original_graph.tasks.values():
unmocked_dataset_types.append(task_node.init.config_output.dataset_type_name)

Check warning on line 132 in python/lsst/pipe/base/tests/mocks/_pipeline_task.py

View check run for this annotation

Codecov / codecov/patch

python/lsst/pipe/base/tests/mocks/_pipeline_task.py#L132

Added line #L132 was not covered by tests
if task_node.log_output is not None:
unmocked_dataset_types.append(task_node.log_output.dataset_type_name)
unmocked_dataset_types.append(task_node.metadata_output.dataset_type_name)

Check warning on line 135 in python/lsst/pipe/base/tests/mocks/_pipeline_task.py

View check run for this annotation

Codecov / codecov/patch

python/lsst/pipe/base/tests/mocks/_pipeline_task.py#L134-L135

Added lines #L134 - L135 were not covered by tests
for original_task_node in original_graph.tasks.values():
config = MockPipelineTaskConfig()
config.original.retarget(original_task_node.task_class)
Expand Down Expand Up @@ -306,14 +317,15 @@
input_dataset = butlerQC.get(ref)
if isinstance(input_dataset, DeferredDatasetHandle):
input_dataset = input_dataset.get()
if not isinstance(input_dataset, MockDataset):
if isinstance(input_dataset, MockDataset):
# To avoid very deep provenance we trim inputs to a
# single level.
input_dataset.quantum = None

Check warning on line 323 in python/lsst/pipe/base/tests/mocks/_pipeline_task.py

View check run for this annotation

Codecov / codecov/patch

python/lsst/pipe/base/tests/mocks/_pipeline_task.py#L323

Added line #L323 was not covered by tests
elif not isinstance(input_dataset, ConvertedUnmockedDataset):
raise TypeError(
f"Expected MockDataset instance for {ref}; "
f"Expected MockDataset or ConvertedUnmockedDataset instance for {ref}; "
f"got {input_dataset!r} of type {type(input_dataset)!r}."
)
# To avoid very deep provenance we trim inputs to a single
# level.
input_dataset.quantum = None
else:
input_dataset = MockDataset(
dataset_id=ref.id,
Expand Down Expand Up @@ -381,22 +393,12 @@
self.unmocked_dataset_types = frozenset(config.unmocked_dataset_types)
for name, connection in self.original.allConnections.items():
if connection.name not in self.unmocked_dataset_types:
if connection.storageClass in (
acc.CONFIG_INIT_OUTPUT_STORAGE_CLASS,
acc.METADATA_OUTPUT_STORAGE_CLASS,
acc.LOG_OUTPUT_STORAGE_CLASS,
):
# We don't mock the automatic output connections, so if
# they're used as an input in any other connection, we
# can't mock them there either.
storage_class_name = connection.storageClass
else:
# We register the mock storage class with the global
# singleton here, but can only put its name in the
# connection. That means the same global singleton (or one
# that also has these registrations) has to be available
# whenever this dataset type is used.
storage_class_name = MockStorageClass.get_or_register_mock(connection.storageClass).name
# We register the mock storage class with the global
# singleton here, but can only put its name in the
# connection. That means the same global singleton (or one
# that also has these registrations) has to be available
# whenever this dataset type is used.
storage_class_name = MockStorageClass.get_or_register_mock(connection.storageClass).name

Check warning on line 401 in python/lsst/pipe/base/tests/mocks/_pipeline_task.py

View check run for this annotation

Codecov / codecov/patch

python/lsst/pipe/base/tests/mocks/_pipeline_task.py#L401

Added line #L401 was not covered by tests
kwargs: dict[str, Any] = {}
if hasattr(connection, "dimensions"):
connection_dimensions = set(connection.dimensions)
Expand Down
48 changes: 42 additions & 6 deletions python/lsst/pipe/base/tests/mocks/_storage_class.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
from __future__ import annotations

__all__ = (
"ConvertedUnmockedDataset",
"MockDataset",
"MockStorageClass",
"MockDatasetQuantum",
Expand Down Expand Up @@ -229,6 +230,40 @@
return super().model_json_schema(*args, **kwargs)


class ConvertedUnmockedDataset(pydantic.BaseModel):
"""A marker class that represents a conversion from a regular in-memory
dataset to a mock storage class.
"""

original_type: str
"""The full Python type of the original unmocked in-memory dataset."""

# Work around the fact that Sphinx chokes on Pydantic docstring formatting,
# when we inherit those docstrings in our public classes.
if "sphinx" in sys.modules:

def copy(self, *args: Any, **kwargs: Any) -> Any:

Check warning on line 245 in python/lsst/pipe/base/tests/mocks/_storage_class.py

View check run for this annotation

Codecov / codecov/patch

python/lsst/pipe/base/tests/mocks/_storage_class.py#L245

Added line #L245 was not covered by tests
"""See `pydantic.BaseModel.copy`."""
return super().copy(*args, **kwargs)

Check warning on line 247 in python/lsst/pipe/base/tests/mocks/_storage_class.py

View check run for this annotation

Codecov / codecov/patch

python/lsst/pipe/base/tests/mocks/_storage_class.py#L247

Added line #L247 was not covered by tests

def model_dump(self, *args: Any, **kwargs: Any) -> Any:

Check warning on line 249 in python/lsst/pipe/base/tests/mocks/_storage_class.py

View check run for this annotation

Codecov / codecov/patch

python/lsst/pipe/base/tests/mocks/_storage_class.py#L249

Added line #L249 was not covered by tests
"""See `pydantic.BaseModel.model_dump`."""
return super().model_dump(*args, **kwargs)

Check warning on line 251 in python/lsst/pipe/base/tests/mocks/_storage_class.py

View check run for this annotation

Codecov / codecov/patch

python/lsst/pipe/base/tests/mocks/_storage_class.py#L251

Added line #L251 was not covered by tests

def model_dump_json(self, *args: Any, **kwargs: Any) -> Any:

Check warning on line 253 in python/lsst/pipe/base/tests/mocks/_storage_class.py

View check run for this annotation

Codecov / codecov/patch

python/lsst/pipe/base/tests/mocks/_storage_class.py#L253

Added line #L253 was not covered by tests
"""See `pydantic.BaseModel.model_dump_json`."""
return super().model_dump(*args, **kwargs)

Check warning on line 255 in python/lsst/pipe/base/tests/mocks/_storage_class.py

View check run for this annotation

Codecov / codecov/patch

python/lsst/pipe/base/tests/mocks/_storage_class.py#L255

Added line #L255 was not covered by tests

def model_copy(self, *args: Any, **kwargs: Any) -> Any:

Check warning on line 257 in python/lsst/pipe/base/tests/mocks/_storage_class.py

View check run for this annotation

Codecov / codecov/patch

python/lsst/pipe/base/tests/mocks/_storage_class.py#L257

Added line #L257 was not covered by tests
"""See `pydantic.BaseModel.model_copy`."""
return super().model_copy(*args, **kwargs)

Check warning on line 259 in python/lsst/pipe/base/tests/mocks/_storage_class.py

View check run for this annotation

Codecov / codecov/patch

python/lsst/pipe/base/tests/mocks/_storage_class.py#L259

Added line #L259 was not covered by tests

@classmethod

Check warning on line 261 in python/lsst/pipe/base/tests/mocks/_storage_class.py

View check run for this annotation

Codecov / codecov/patch

python/lsst/pipe/base/tests/mocks/_storage_class.py#L261

Added line #L261 was not covered by tests
def model_json_schema(cls, *args: Any, **kwargs: Any) -> Any:
"""See `pydantic.BaseModel.model_json_schema`."""
return super().model_json_schema(*args, **kwargs)

Check warning on line 264 in python/lsst/pipe/base/tests/mocks/_storage_class.py

View check run for this annotation

Codecov / codecov/patch

python/lsst/pipe/base/tests/mocks/_storage_class.py#L264

Added line #L264 was not covered by tests


class MockDatasetQuantum(pydantic.BaseModel):
"""Description of the quantum that produced a mock dataset.

Expand All @@ -242,7 +277,7 @@
data_id: dict[str, DataIdValue]
"""Data ID for the quantum."""

inputs: dict[str, list[MockDataset]]
inputs: dict[str, list[MockDataset | ConvertedUnmockedDataset]]
"""Mock datasets provided as input to the quantum.

Keys are task-internal connection names, not dataset type names.
Expand Down Expand Up @@ -410,16 +445,17 @@
def can_convert(self, other: StorageClass) -> bool:
# Docstring inherited.
if not isinstance(other, MockStorageClass):
return False
# Allow conversions from an original type (and others compatible
# with it) to a mock, to allow for cases where an upstream task
# did not use a mock to write something but the downstream one is
# trying to us a mock to read it.
return self.original.can_convert(other)

Check warning on line 452 in python/lsst/pipe/base/tests/mocks/_storage_class.py

View check run for this annotation

Codecov / codecov/patch

python/lsst/pipe/base/tests/mocks/_storage_class.py#L452

Added line #L452 was not covered by tests
return self.original.can_convert(other.original)

def coerce_type(self, incorrect: Any) -> Any:
# Docstring inherited.
if not isinstance(incorrect, MockDataset):
raise TypeError(
f"Mock storage class {self.name!r} can only convert in-memory datasets "
f"corresponding to other mock storage classes, not {incorrect!r}."
)
return ConvertedUnmockedDataset(original_type=get_full_type_name(incorrect))

Check warning on line 458 in python/lsst/pipe/base/tests/mocks/_storage_class.py

View check run for this annotation

Codecov / codecov/patch

python/lsst/pipe/base/tests/mocks/_storage_class.py#L458

Added line #L458 was not covered by tests
factory = StorageClassFactory()
other_storage_class = factory.getStorageClass(incorrect.storage_class)
assert isinstance(other_storage_class, MockStorageClass), "Should not get a MockDataset otherwise."
Expand Down
Loading