diff --git a/api/src/opentrons/execute.py b/api/src/opentrons/execute.py index 998d6bc6597..90bf1e43e0f 100644 --- a/api/src/opentrons/execute.py +++ b/api/src/opentrons/execute.py @@ -23,7 +23,9 @@ Union, ) -from opentrons_shared_data.labware.labware_definition import LabwareDefinition +from opentrons_shared_data.labware.labware_definition import ( + labware_definition_type_adapter, +) from opentrons_shared_data.robot.types import RobotType from opentrons import protocol_api, __version__, should_use_ot3 @@ -560,7 +562,7 @@ def _create_live_context_pe( # Non-async would use call_soon_threadsafe(), which makes the waiting harder. async def add_all_extra_labware() -> None: for labware_definition_dict in extra_labware.values(): - labware_definition = LabwareDefinition.model_validate( + labware_definition = labware_definition_type_adapter.validate_python( labware_definition_dict ) pe.add_labware_definition(labware_definition) diff --git a/api/src/opentrons/hardware_control/instruments/ot2/instrument_calibration.py b/api/src/opentrons/hardware_control/instruments/ot2/instrument_calibration.py index 39dc17504e8..ddd036e01ae 100644 --- a/api/src/opentrons/hardware_control/instruments/ot2/instrument_calibration.py +++ b/api/src/opentrons/hardware_control/instruments/ot2/instrument_calibration.py @@ -9,7 +9,7 @@ from opentrons.types import Mount, Point from opentrons.hardware_control.types import OT3Mount -from opentrons_shared_data.labware.labware_definition import LabwareDefinition +from opentrons_shared_data.labware.labware_definition import LabwareDefinition2 if typing.TYPE_CHECKING: from opentrons_shared_data.pipette.types import LabwareUri @@ -119,12 +119,9 @@ def save_pipette_offset_calibration( # TODO (lc 09-26-2022) We should ensure that only LabwareDefinition models are passed # into this function instead of a mixture of TypeDicts and BaseModels def load_tip_length_for_pipette( - pipette_id: str, tiprack: typing.Union["TypeDictLabwareDef2", LabwareDefinition] + pipette_id: str, tiprack: typing.Union["TypeDictLabwareDef2", LabwareDefinition2] ) -> TipLengthCalibration: - if isinstance(tiprack, LabwareDefinition): - # todo(mm, 2025-02-13): This is only correct for schema 2 labware. - # The LabwareDefinition union member needs to be narrowed to LabwareDefinition2, - # which doesn't exist yet (https://opentrons.atlassian.net/browse/EXEC-1206). + if isinstance(tiprack, LabwareDefinition2): tiprack = typing.cast( "TypeDictLabwareDef2", tiprack.model_dump( diff --git a/api/src/opentrons/protocol_api/core/engine/protocol.py b/api/src/opentrons/protocol_api/core/engine/protocol.py index aa7b1821eee..d78c78c1da9 100644 --- a/api/src/opentrons/protocol_api/core/engine/protocol.py +++ b/api/src/opentrons/protocol_api/core/engine/protocol.py @@ -8,7 +8,9 @@ from opentrons.protocol_engine.commands import LoadModuleResult from opentrons_shared_data.deck.types import DeckDefinitionV5, SlotDefV3 -from opentrons_shared_data.labware.labware_definition import LabwareDefinition +from opentrons_shared_data.labware.labware_definition import ( + labware_definition_type_adapter, +) from opentrons_shared_data.labware.types import LabwareDefinition as LabwareDefDict from opentrons_shared_data import liquid_classes from opentrons_shared_data.liquid_classes.liquid_class_definition import ( @@ -196,7 +198,7 @@ def add_labware_definition( ) -> LabwareLoadParams: """Add a labware definition to the set of loadable definitions.""" uri = self._engine_client.add_labware_definition( - LabwareDefinition.model_validate(definition) + labware_definition_type_adapter.validate_python(definition) ) return LabwareLoadParams.from_uri(uri) diff --git a/api/src/opentrons/protocol_api/core/engine/well.py b/api/src/opentrons/protocol_api/core/engine/well.py index f1ddf5b47ca..f371709cf62 100644 --- a/api/src/opentrons/protocol_api/core/engine/well.py +++ b/api/src/opentrons/protocol_api/core/engine/well.py @@ -44,17 +44,27 @@ def labware_id(self) -> str: @property def diameter(self) -> Optional[float]: """Get the well's diameter, if circular.""" - return self._definition.diameter + return ( + self._definition.diameter if self._definition.shape == "circular" else None + ) @property def length(self) -> Optional[float]: """Get the well's length, if rectangular.""" - return self._definition.xDimension + return ( + self._definition.xDimension + if self._definition.shape == "rectangular" + else None + ) @property def width(self) -> Optional[float]: """Get the well's width, if rectangular.""" - return self._definition.yDimension + return ( + self._definition.yDimension + if self._definition.shape == "rectangular" + else None + ) @property def depth(self) -> float: diff --git a/api/src/opentrons/protocol_engine/commands/load_labware.py b/api/src/opentrons/protocol_engine/commands/load_labware.py index 8086599c42c..504ef99578a 100644 --- a/api/src/opentrons/protocol_engine/commands/load_labware.py +++ b/api/src/opentrons/protocol_engine/commands/load_labware.py @@ -5,9 +5,13 @@ from pydantic import BaseModel, Field from pydantic.json_schema import SkipJsonSchema -from typing_extensions import Literal, TypeGuard +from typing_extensions import Literal, TypeGuard, assert_type -from opentrons_shared_data.labware.labware_definition import LabwareDefinition +from opentrons_shared_data.labware.labware_definition import ( + LabwareDefinition, + LabwareDefinition2, + LabwareDefinition3, +) from ..errors import LabwareIsNotAllowedInLocationError from ..resources import labware_validation, fixture_validation @@ -184,17 +188,29 @@ async def execute( # noqa: C901 bottom_labware_id=verified_location.labwareId, ) # Validate load location is valid for lids - if ( - labware_validation.validate_definition_is_lid( - definition=loaded_labware.definition - ) - and loaded_labware.definition.compatibleParentLabware is not None - and self._state_view.labware.get_load_name(verified_location.labwareId) - not in loaded_labware.definition.compatibleParentLabware + if labware_validation.validate_definition_is_lid( + definition=loaded_labware.definition ): - raise ValueError( - f"Labware Lid {params.loadName} may not be loaded on parent labware {self._state_view.labware.get_display_name(verified_location.labwareId)}." - ) + # This parent is assumed to be compatible, unless the lid enumerates + # all its compatible parents and this parent is missing from the list. + if isinstance(loaded_labware.definition, LabwareDefinition2): + # Labware schema 2 has no compatibleParentLabware list. + parent_is_incompatible = False + else: + assert_type(loaded_labware.definition, LabwareDefinition3) + parent_is_incompatible = ( + loaded_labware.definition.compatibleParentLabware is not None + and self._state_view.labware.get_load_name( + verified_location.labwareId + ) + not in loaded_labware.definition.compatibleParentLabware + ) + + if parent_is_incompatible: + raise ValueError( + f"Labware Lid {params.loadName} may not be loaded on parent labware" + f" {self._state_view.labware.get_display_name(verified_location.labwareId)}." + ) # Validate labware for the absorbance reader if self._is_loading_to_module( diff --git a/api/src/opentrons/protocol_engine/execution/equipment.py b/api/src/opentrons/protocol_engine/execution/equipment.py index 31232079a9c..bac0713eb54 100644 --- a/api/src/opentrons/protocol_engine/execution/equipment.py +++ b/api/src/opentrons/protocol_engine/execution/equipment.py @@ -2,8 +2,13 @@ from dataclasses import dataclass from typing import Optional, overload, List +from typing_extensions import assert_type -from opentrons_shared_data.labware.labware_definition import LabwareDefinition +from opentrons_shared_data.labware.labware_definition import ( + LabwareDefinition, + LabwareDefinition2, + LabwareDefinition3, +) from opentrons_shared_data.pipette.types import PipetteNameType from opentrons.calibration_storage.helpers import uri_from_details @@ -406,7 +411,7 @@ async def load_module( definition=attached_module.definition, ) - async def load_lids( + async def load_lids( # noqa: C901 self, load_name: str, namespace: str, @@ -454,18 +459,23 @@ async def load_lids( f"Requested quantity {quantity} is greater than the stack limit of {stack_limit} provided by definition for {load_name}." ) - # Allow propagation of ModuleNotLoadedError. - if ( - isinstance(location, DeckSlotLocation) - and definition.parameters.isDeckSlotCompatible is not None - and not definition.parameters.isDeckSlotCompatible - ): + if isinstance(definition, LabwareDefinition2): + is_deck_slot_compatible = True + else: + assert_type(definition, LabwareDefinition3) + is_deck_slot_compatible = ( + True + if definition.parameters.isDeckSlotCompatible is None + else definition.parameters.isDeckSlotCompatible + ) + + if isinstance(location, DeckSlotLocation) and not is_deck_slot_compatible: raise ValueError( f"Lid Labware {load_name} cannot be loaded onto a Deck Slot." ) - load_labware_data_list = [] - ids = [] + load_labware_data_list: list[LoadedLabwareData] = [] + ids: list[str] = [] if labware_ids is not None: if len(labware_ids) < quantity: raise ValueError( diff --git a/api/src/opentrons/protocol_engine/resources/labware_data_provider.py b/api/src/opentrons/protocol_engine/resources/labware_data_provider.py index b71f7e6dac1..52bb0d6f42b 100644 --- a/api/src/opentrons/protocol_engine/resources/labware_data_provider.py +++ b/api/src/opentrons/protocol_engine/resources/labware_data_provider.py @@ -6,7 +6,11 @@ import logging from anyio import to_thread -from opentrons_shared_data.labware.labware_definition import LabwareDefinition +from opentrons_shared_data.labware.labware_definition import ( + LabwareDefinition, + LabwareDefinition3, + labware_definition_type_adapter, +) from opentrons.protocols.labware import get_labware_definition @@ -45,7 +49,7 @@ async def get_labware_definition( def _get_labware_definition_sync( load_name: str, namespace: str, version: int ) -> LabwareDefinition: - return LabwareDefinition.model_validate( + return labware_definition_type_adapter.validate_python( get_labware_definition(load_name, namespace, version) ) @@ -73,15 +77,30 @@ def _get_calibrated_tip_length_sync( labware_definition: LabwareDefinition, nominal_fallback: float, ) -> float: - try: - return instr_cal.load_tip_length_for_pipette( - pipette_serial, labware_definition - ).tip_length - - except TipLengthCalNotFound as e: - message = ( - f"No calibrated tip length found for {pipette_serial}," - f" using nominal fallback value of {nominal_fallback}" + if isinstance(labware_definition, LabwareDefinition3): + # FIXME(mm, 2025-02-19): This needs to be resolved for v8.4.0. + # Tip length calibration internals don't yet support schema 3 because + # it's probably an incompatible change at the filesystem level + # (not downgrade-safe), and because robot-server's calibration sessions + # are built atop opentrons.protocol_api.core.legacy, which does not (yet?) + # support labware schema 3. + # https://opentrons.atlassian.net/browse/EXEC-1230 + log.warning( + f"Tip rack" + f" {labware_definition.namespace}/{labware_definition.parameters.loadName}/{labware_definition.version}" + f" has schema 3, so tip length calibration is currently unsupported." + f" Using nominal fallback of {nominal_fallback}." ) - log.debug(message, exc_info=e) return nominal_fallback + else: + try: + return instr_cal.load_tip_length_for_pipette( + pipette_serial, labware_definition + ).tip_length + except TipLengthCalNotFound as e: + message = ( + f"No calibrated tip length found for {pipette_serial}," + f" using nominal fallback value of {nominal_fallback}" + ) + log.debug(message, exc_info=e) + return nominal_fallback diff --git a/api/src/opentrons/protocol_engine/state/geometry.py b/api/src/opentrons/protocol_engine/state/geometry.py index 63a447db647..08ddeb3b107 100644 --- a/api/src/opentrons/protocol_engine/state/geometry.py +++ b/api/src/opentrons/protocol_engine/state/geometry.py @@ -660,7 +660,7 @@ def get_nominal_tip_geometry( return TipGeometry( length=effective_length, - diameter=well_def.diameter, # type: ignore[arg-type] + diameter=well_def.diameter, # TODO(mc, 2020-11-12): WellDefinition type says totalLiquidVolume # is a float, but hardware controller expects an int volume=int(well_def.totalLiquidVolume), diff --git a/api/src/opentrons/protocol_engine/state/labware.py b/api/src/opentrons/protocol_engine/state/labware.py index 56f4d1049e2..7b803882a01 100644 --- a/api/src/opentrons/protocol_engine/state/labware.py +++ b/api/src/opentrons/protocol_engine/state/labware.py @@ -12,10 +12,10 @@ Sequence, Tuple, NamedTuple, - cast, Union, overload, ) +from typing_extensions import assert_never from opentrons.protocol_engine.state import update_types from opentrons_shared_data.deck.types import DeckDefinitionV5 @@ -23,8 +23,12 @@ from opentrons_shared_data.labware.labware_definition import ( InnerWellGeometry, LabwareDefinition, + LabwareDefinition2, LabwareRole, - WellDefinition, + WellDefinition2, + WellDefinition3, + CircularWellDefinition2, + RectangularWellDefinition2, ) from opentrons_shared_data.pipette.types import LabwareUri @@ -91,6 +95,9 @@ _PLATE_READER_MAX_LABWARE_Z_MM = 16 +_WellDefinition = WellDefinition2 | WellDefinition3 + + class LabwareLoadParams(NamedTuple): """Parameters required to load a labware in Protocol Engine.""" @@ -574,7 +581,7 @@ def get_well_definition( self, labware_id: str, well_name: Optional[str] = None, - ) -> WellDefinition: + ) -> WellDefinition2 | WellDefinition3: """Get a well's definition by labware and well name. If `well_name` is omitted, the first well in the labware @@ -596,21 +603,29 @@ def get_well_geometry( ) -> InnerWellGeometry: """Get a well's inner geometry by labware and well name.""" labware_def = self.get_definition(labware_id) - if labware_def.innerLabwareGeometry is None: + if ( + isinstance(labware_def, LabwareDefinition2) + or labware_def.innerLabwareGeometry is None + ): raise errors.IncompleteLabwareDefinitionError( message=f"No innerLabwareGeometry found in labware definition for labware_id: {labware_id}." ) well_def = self.get_well_definition(labware_id, well_name) - well_id = well_def.geometryDefinitionId - if well_id is None: + # Assert for type-checking. We expect the well definitions from schema *3*, specifically. + # This should always pass because we exclude LabwareDefinition2 above. + assert not isinstance( + well_def, (RectangularWellDefinition2, CircularWellDefinition2) + ) + geometry_id = well_def.geometryDefinitionId + if geometry_id is None: raise errors.IncompleteWellDefinitionError( message=f"No geometryDefinitionId found in well definition for well: {well_name} in labware_id: {labware_id}" ) else: - well_geometry = labware_def.innerLabwareGeometry.get(well_id) + well_geometry = labware_def.innerLabwareGeometry.get(geometry_id) if well_geometry is None: raise errors.IncompleteLabwareDefinitionError( - message=f"No innerLabwareGeometry found in labware definition for well_id: {well_id} in labware_id: {labware_id}" + message=f"No innerLabwareGeometry found in labware definition for well_id: {geometry_id} in labware_id: {labware_id}" ) return well_geometry @@ -629,12 +644,13 @@ def get_well_size( """ well_definition = self.get_well_definition(labware_id, well_name) - if well_definition.diameter is not None: + if well_definition.shape == "circular": x_size = y_size = well_definition.diameter + elif well_definition.shape == "rectangular": + x_size = well_definition.xDimension + y_size = well_definition.yDimension else: - # If diameter is None we know these values will be floats - x_size = cast(float, well_definition.xDimension) - y_size = cast(float, well_definition.yDimension) + assert_never(well_definition.shape) return x_size, y_size, well_definition.depth @@ -1167,7 +1183,7 @@ def get_grip_height_from_labware_bottom( ) @staticmethod - def _max_x_of_well(well_defn: WellDefinition) -> float: + def _max_x_of_well(well_defn: _WellDefinition) -> float: if well_defn.shape == "rectangular": return well_defn.x + (well_defn.xDimension or 0) / 2 elif well_defn.shape == "circular": @@ -1176,7 +1192,7 @@ def _max_x_of_well(well_defn: WellDefinition) -> float: return well_defn.x @staticmethod - def _min_x_of_well(well_defn: WellDefinition) -> float: + def _min_x_of_well(well_defn: _WellDefinition) -> float: if well_defn.shape == "rectangular": return well_defn.x - (well_defn.xDimension or 0) / 2 elif well_defn.shape == "circular": @@ -1185,7 +1201,7 @@ def _min_x_of_well(well_defn: WellDefinition) -> float: return 0 @staticmethod - def _max_y_of_well(well_defn: WellDefinition) -> float: + def _max_y_of_well(well_defn: _WellDefinition) -> float: if well_defn.shape == "rectangular": return well_defn.y + (well_defn.yDimension or 0) / 2 elif well_defn.shape == "circular": @@ -1194,7 +1210,7 @@ def _max_y_of_well(well_defn: WellDefinition) -> float: return 0 @staticmethod - def _min_y_of_well(well_defn: WellDefinition) -> float: + def _min_y_of_well(well_defn: _WellDefinition) -> float: if well_defn.shape == "rectangular": return well_defn.y - (well_defn.yDimension or 0) / 2 elif well_defn.shape == "circular": @@ -1203,7 +1219,7 @@ def _min_y_of_well(well_defn: WellDefinition) -> float: return 0 @staticmethod - def _max_z_of_well(well_defn: WellDefinition) -> float: + def _max_z_of_well(well_defn: _WellDefinition) -> float: return well_defn.z + well_defn.depth def get_well_bbox(self, labware_definition: LabwareDefinition) -> Dimensions: diff --git a/api/src/opentrons/protocol_reader/extract_labware_definitions.py b/api/src/opentrons/protocol_reader/extract_labware_definitions.py index 8ee4c820e87..f17ca841065 100644 --- a/api/src/opentrons/protocol_reader/extract_labware_definitions.py +++ b/api/src/opentrons/protocol_reader/extract_labware_definitions.py @@ -6,7 +6,10 @@ import anyio -from opentrons_shared_data.labware.labware_definition import LabwareDefinition +from opentrons_shared_data.labware.labware_definition import ( + LabwareDefinition, + labware_definition_type_adapter, +) from .protocol_source import ProtocolFileRole, ProtocolSource, ProtocolType @@ -42,7 +45,7 @@ async def extract_labware_definitions( async def _extract_from_labware_file(path: Path) -> LabwareDefinition: def _do_parse() -> LabwareDefinition: - return LabwareDefinition.model_validate_json(path.read_bytes()) + return labware_definition_type_adapter.validate_json(path.read_bytes()) return await anyio.to_thread.run_sync(_do_parse) @@ -55,7 +58,8 @@ def extract_sync(path: Path) -> List[LabwareDefinition]: # which require this labwareDefinitions key. unvalidated_definitions = json_contents["labwareDefinitions"].values() validated_definitions = [ - LabwareDefinition.model_validate(u) for u in unvalidated_definitions + labware_definition_type_adapter.validate_python(u) + for u in unvalidated_definitions ] return validated_definitions diff --git a/api/src/opentrons/protocol_reader/file_format_validator.py b/api/src/opentrons/protocol_reader/file_format_validator.py index ca664eced92..be93fa55ef1 100644 --- a/api/src/opentrons/protocol_reader/file_format_validator.py +++ b/api/src/opentrons/protocol_reader/file_format_validator.py @@ -6,7 +6,9 @@ import anyio from pydantic import ValidationError as PydanticValidationError -from opentrons_shared_data.labware.labware_definition import LabwareDefinition +from opentrons_shared_data.labware.labware_definition import ( + labware_definition_type_adapter, +) from opentrons_shared_data.protocol.models import ( ProtocolSchemaV6 as JsonProtocolV6, ProtocolSchemaV7 as JsonProtocolV7, @@ -60,7 +62,7 @@ async def validate(files: Iterable[IdentifiedFile]) -> None: async def _validate_labware_definition(info: IdentifiedLabwareDefinition) -> None: def validate_sync() -> None: try: - LabwareDefinition.model_validate(info.unvalidated_json) + labware_definition_type_adapter.validate_python(info.unvalidated_json) except PydanticValidationError as e: raise FileFormatValidationError( message=f"{info.original_file.name} could not be read as a labware definition.", diff --git a/api/src/opentrons/protocol_runner/legacy_command_mapper.py b/api/src/opentrons/protocol_runner/legacy_command_mapper.py index afb06075e2a..4df76910a34 100644 --- a/api/src/opentrons/protocol_runner/legacy_command_mapper.py +++ b/api/src/opentrons/protocol_runner/legacy_command_mapper.py @@ -38,7 +38,9 @@ StateUpdate, ) -from opentrons_shared_data.labware.labware_definition import LabwareDefinition +from opentrons_shared_data.labware.labware_definition import ( + labware_definition_type_adapter, +) from opentrons_shared_data.errors import ErrorCodes, EnumeratedError, PythonException @@ -659,7 +661,7 @@ def _map_labware_load( notes=[], result=pe_commands.LoadLabwareResult.model_construct( labwareId=labware_id, - definition=LabwareDefinition.model_validate( + definition=labware_definition_type_adapter.validate_python( labware_load_info.labware_definition ), offsetId=labware_load_info.offset_id, diff --git a/api/src/opentrons/simulate.py b/api/src/opentrons/simulate.py index bed24c68731..a3fa51684e4 100644 --- a/api/src/opentrons/simulate.py +++ b/api/src/opentrons/simulate.py @@ -71,7 +71,9 @@ should_load_fixed_trash_labware_for_python_protocol, ) from opentrons.protocols.api_support.types import APIVersion -from opentrons_shared_data.labware.labware_definition import LabwareDefinition +from opentrons_shared_data.labware.labware_definition import ( + labware_definition_type_adapter, +) from .util import entrypoint_util @@ -829,7 +831,7 @@ def _create_live_context_pe( # Non-async would use call_soon_threadsafe(), which makes the waiting harder. async def add_all_extra_labware() -> None: for labware_definition_dict in extra_labware.values(): - labware_definition = LabwareDefinition.model_validate( + labware_definition = labware_definition_type_adapter.validate_python( labware_definition_dict ) pe.add_labware_definition(labware_definition) diff --git a/api/tests/opentrons/hardware_control/instruments/test_instrument_calibration.py b/api/tests/opentrons/hardware_control/instruments/test_instrument_calibration.py index 6bdf3e057fd..82ee0c0d071 100644 --- a/api/tests/opentrons/hardware_control/instruments/test_instrument_calibration.py +++ b/api/tests/opentrons/hardware_control/instruments/test_instrument_calibration.py @@ -11,8 +11,8 @@ LabwareDefinition2 as LabwareDef2Dict, ) from opentrons_shared_data.labware.labware_definition import ( - LabwareDefinition, - Parameters, + LabwareDefinition2, + Parameters2, ) @@ -53,12 +53,12 @@ def tip_rack_dict() -> LabwareDef2Dict: @pytest.fixture -def tip_rack_model() -> LabwareDefinition: +def tip_rack_model() -> LabwareDefinition2: """Get a tip rack Pydantic model definition value object.""" - return LabwareDefinition.model_construct( # type: ignore[call-arg] + return LabwareDefinition2.model_construct( # type: ignore[call-arg] namespace="test", version=1, - parameters=Parameters.model_construct( # type: ignore[call-arg] + parameters=Parameters2.model_construct( # type: ignore[call-arg] loadName="cool-labware", tipOverlap=None, # add a None value to validate serialization to dictionary ), @@ -75,7 +75,7 @@ def tip_rack_model() -> LabwareDefinition: def test_load_tip_length( decoy: Decoy, tip_rack_dict: LabwareDef2Dict, - tip_rack_definition: Union[LabwareDef2Dict, LabwareDefinition], + tip_rack_definition: Union[LabwareDef2Dict, LabwareDefinition2], ) -> None: """Test that a tip length can be laoded for a pipette / tiprack combination.""" tip_length_data = v1_models.TipLengthModel( diff --git a/api/tests/opentrons/protocol_api/core/engine/test_labware_core.py b/api/tests/opentrons/protocol_api/core/engine/test_labware_core.py index a858cb6b680..31ee86a04b9 100644 --- a/api/tests/opentrons/protocol_api/core/engine/test_labware_core.py +++ b/api/tests/opentrons/protocol_api/core/engine/test_labware_core.py @@ -10,9 +10,9 @@ LabwareUri, ) from opentrons_shared_data.labware.labware_definition import ( - LabwareDefinition, + LabwareDefinition2, LabwareRole, - Parameters as LabwareDefinitionParameters, + Parameters2 as LabwareDefinition2Parameters, Metadata as LabwareDefinitionMetadata, ) @@ -33,14 +33,14 @@ @pytest.fixture -def labware_definition() -> LabwareDefinition: +def labware_definition() -> LabwareDefinition2: """Get a LabwareDefinition value object to use in tests.""" - return LabwareDefinition.model_construct(ordering=[]) # type: ignore[call-arg] + return LabwareDefinition2.model_construct(ordering=[]) # type: ignore[call-arg] @pytest.fixture def mock_engine_client( - decoy: Decoy, labware_definition: LabwareDefinition + decoy: Decoy, labware_definition: LabwareDefinition2 ) -> EngineClient: """Get a mock ProtocolEngine synchronous client.""" engine_client = decoy.mock(cls=EngineClient) @@ -59,10 +59,10 @@ def subject(mock_engine_client: EngineClient) -> LabwareCore: @pytest.mark.parametrize( "labware_definition", [ - LabwareDefinition.model_construct( # type: ignore[call-arg] + LabwareDefinition2.model_construct( # type: ignore[call-arg] namespace="hello", version=42, - parameters=LabwareDefinitionParameters.model_construct(loadName="world"), # type: ignore[call-arg] + parameters=LabwareDefinition2Parameters.model_construct(loadName="world"), # type: ignore[call-arg] ordering=[], ) ], @@ -76,10 +76,10 @@ def test_get_load_params(subject: LabwareCore) -> None: @pytest.mark.parametrize( "labware_definition", [ - LabwareDefinition.model_construct( # type: ignore[call-arg] + LabwareDefinition2.model_construct( # type: ignore[call-arg] namespace="hello", version=42, - parameters=LabwareDefinitionParameters.model_construct(loadName="world"), # type: ignore[call-arg] + parameters=LabwareDefinition2Parameters.model_construct(loadName="world"), # type: ignore[call-arg] ordering=[], metadata=LabwareDefinitionMetadata.model_construct( # type: ignore[call-arg] displayName="what a cool labware" @@ -91,7 +91,7 @@ def test_set_calibration_succeeds_in_ok_location( decoy: Decoy, subject: LabwareCore, mock_engine_client: EngineClient, - labware_definition: LabwareDefinition, + labware_definition: LabwareDefinition2, ) -> None: """It should pass along an AddLabwareOffset if possible.""" decoy.when( @@ -132,10 +132,10 @@ def test_set_calibration_succeeds_in_ok_location( @pytest.mark.parametrize( "labware_definition", [ - LabwareDefinition.model_construct( # type: ignore[call-arg] + LabwareDefinition2.model_construct( # type: ignore[call-arg] namespace="hello", version=42, - parameters=LabwareDefinitionParameters.model_construct(loadName="world"), # type: ignore[call-arg] + parameters=LabwareDefinition2Parameters.model_construct(loadName="world"), # type: ignore[call-arg] ordering=[], ) ], @@ -144,7 +144,7 @@ def test_set_calibration_fails_in_bad_location( decoy: Decoy, subject: LabwareCore, mock_engine_client: EngineClient, - labware_definition: LabwareDefinition, + labware_definition: LabwareDefinition2, ) -> None: """It should raise if you attempt to set calibration when the labware is not on deck.""" decoy.when( @@ -166,9 +166,9 @@ def test_set_calibration_fails_in_bad_location( @pytest.mark.parametrize( "labware_definition", [ - LabwareDefinition.model_construct( # type: ignore[call-arg] + LabwareDefinition2.model_construct( # type: ignore[call-arg] namespace="hello", - parameters=LabwareDefinitionParameters.model_construct(loadName="world"), # type: ignore[call-arg] + parameters=LabwareDefinition2Parameters.model_construct(loadName="world"), # type: ignore[call-arg] ordering=[], allowedRoles=[], stackingOffsetWithLabware={}, @@ -212,7 +212,7 @@ def test_get_user_display_name(decoy: Decoy, mock_engine_client: EngineClient) - @pytest.mark.parametrize( "labware_definition", [ - LabwareDefinition.model_construct( # type: ignore[call-arg] + LabwareDefinition2.model_construct( # type: ignore[call-arg] ordering=[], metadata=LabwareDefinitionMetadata.model_construct( # type: ignore[call-arg] displayName="Cool Display Name" @@ -230,8 +230,8 @@ def test_get_display_name(subject: LabwareCore) -> None: @pytest.mark.parametrize( "labware_definition", [ - LabwareDefinition.model_construct( # type: ignore[call-arg] - parameters=LabwareDefinitionParameters.model_construct( # type: ignore[call-arg] + LabwareDefinition2.model_construct( # type: ignore[call-arg] + parameters=LabwareDefinition2Parameters.model_construct( # type: ignore[call-arg] loadName="load-name" ), ), @@ -260,9 +260,9 @@ def test_get_name_display_name(decoy: Decoy, mock_engine_client: EngineClient) - @pytest.mark.parametrize( "labware_definition", [ - LabwareDefinition.model_construct( # type: ignore[call-arg] + LabwareDefinition2.model_construct( # type: ignore[call-arg] ordering=[], - parameters=LabwareDefinitionParameters.model_construct(isTiprack=True), # type: ignore[call-arg] + parameters=LabwareDefinition2Parameters.model_construct(isTiprack=True), # type: ignore[call-arg] ) ], ) @@ -277,13 +277,13 @@ def test_is_tip_rack(subject: LabwareCore) -> None: argnames=["labware_definition", "expected_result"], argvalues=[ ( - LabwareDefinition.model_construct( # type: ignore[call-arg] + LabwareDefinition2.model_construct( # type: ignore[call-arg] ordering=[], allowedRoles=[LabwareRole.adapter] ), True, ), ( - LabwareDefinition.model_construct( # type: ignore[call-arg] + LabwareDefinition2.model_construct( # type: ignore[call-arg] ordering=[], allowedRoles=[LabwareRole.labware] ), False, @@ -300,7 +300,7 @@ def test_is_adapter(expected_result: bool, subject: LabwareCore) -> None: @pytest.mark.parametrize( "labware_definition", [ - LabwareDefinition.model_construct( # type: ignore[call-arg] + LabwareDefinition2.model_construct( # type: ignore[call-arg] ordering=[["A1", "B1"], ["A2", "B2"]], ) ], @@ -360,9 +360,9 @@ def test_get_next_tip( @pytest.mark.parametrize( "labware_definition", [ - LabwareDefinition.model_construct( # type: ignore[call-arg] + LabwareDefinition2.model_construct( # type: ignore[call-arg] ordering=[], - parameters=LabwareDefinitionParameters.model_construct(isTiprack=True), # type: ignore[call-arg] + parameters=LabwareDefinition2Parameters.model_construct(isTiprack=True), # type: ignore[call-arg] ) ], ) @@ -377,9 +377,9 @@ def test_reset_tips( @pytest.mark.parametrize( "labware_definition", [ - LabwareDefinition.model_construct( # type: ignore[call-arg] + LabwareDefinition2.model_construct( # type: ignore[call-arg] ordering=[], - parameters=LabwareDefinitionParameters.model_construct(isTiprack=False), # type: ignore[call-arg] + parameters=LabwareDefinition2Parameters.model_construct(isTiprack=False), # type: ignore[call-arg] metadata=LabwareDefinitionMetadata.model_construct( # type: ignore[call-arg] displayName="Cool Display Name" ), @@ -436,9 +436,9 @@ def test_get_calibrated_offset( @pytest.mark.parametrize( "labware_definition", [ - LabwareDefinition.model_construct( # type: ignore[call-arg] + LabwareDefinition2.model_construct( # type: ignore[call-arg] ordering=[], - parameters=LabwareDefinitionParameters.model_construct(quirks=["quirk"]), # type: ignore[call-arg] + parameters=LabwareDefinition2Parameters.model_construct(quirks=["quirk"]), # type: ignore[call-arg] ) ], ) diff --git a/api/tests/opentrons/protocol_api/core/engine/test_protocol_core.py b/api/tests/opentrons/protocol_api/core/engine/test_protocol_core.py index 8fffba98c54..bd6e8bb6ae6 100644 --- a/api/tests/opentrons/protocol_api/core/engine/test_protocol_core.py +++ b/api/tests/opentrons/protocol_api/core/engine/test_protocol_core.py @@ -20,7 +20,10 @@ LabwareDefinition as LabwareDefDict, LabwareUri, ) -from opentrons_shared_data.labware.labware_definition import LabwareDefinition +from opentrons_shared_data.labware.labware_definition import ( + LabwareDefinition2, + labware_definition_type_adapter, +) from opentrons_shared_data.robot.types import RobotType from opentrons.types import DeckSlotName, StagingSlotName, Mount, MountType, Point @@ -181,7 +184,7 @@ def subject( decoy.when( mock_engine_client.state.labware.get_definition("fixed-trash-123") ).then_return( - LabwareDefinition.model_construct(ordering=[["A1"]]) # type: ignore[call-arg] + LabwareDefinition2.model_construct(ordering=[["A1"]]) # type: ignore[call-arg] ) return ProtocolCore( @@ -358,15 +361,15 @@ def test_load_labware( ) ) ).then_return( - commands.LoadLabwareResult( + commands.LoadLabwareResult.model_construct( labwareId="abc123", - definition=LabwareDefinition.model_construct(), # type: ignore[call-arg] + definition=LabwareDefinition2.model_construct(), # type: ignore[call-arg] offsetId=None, ) ) decoy.when(mock_engine_client.state.labware.get_definition("abc123")).then_return( - LabwareDefinition.model_construct(ordering=[]) # type: ignore[call-arg] + LabwareDefinition2.model_construct(ordering=[]) # type: ignore[call-arg] ) result = subject.load_labware( @@ -432,15 +435,15 @@ def test_load_labware_on_staging_slot( ) ) ).then_return( - commands.LoadLabwareResult( + commands.LoadLabwareResult.model_construct( labwareId="abc123", - definition=LabwareDefinition.model_construct(), # type: ignore[call-arg] + definition=LabwareDefinition2.model_construct(), # type: ignore[call-arg] offsetId=None, ) ) decoy.when(mock_engine_client.state.labware.get_definition("abc123")).then_return( - LabwareDefinition.model_construct(ordering=[]) # type: ignore[call-arg] + LabwareDefinition2.model_construct(ordering=[]) # type: ignore[call-arg] ) result = subject.load_labware( @@ -509,15 +512,15 @@ def test_load_labware_on_labware( ) ) ).then_return( - commands.LoadLabwareResult( + commands.LoadLabwareResult.model_construct( labwareId="abc123", - definition=LabwareDefinition.model_construct(), # type: ignore[call-arg] + definition=LabwareDefinition2.model_construct(), # type: ignore[call-arg] offsetId=None, ) ) decoy.when(mock_engine_client.state.labware.get_definition("abc123")).then_return( - LabwareDefinition.model_construct(ordering=[]) # type: ignore[call-arg] + LabwareDefinition2.model_construct(ordering=[]) # type: ignore[call-arg] ) decoy.when( @@ -579,15 +582,15 @@ def test_load_labware_off_deck( ) ) ).then_return( - commands.LoadLabwareResult( + commands.LoadLabwareResult.model_construct( labwareId="abc123", - definition=LabwareDefinition.model_construct(), # type: ignore[call-arg] + definition=LabwareDefinition2.model_construct(), # type: ignore[call-arg] offsetId=None, ) ) decoy.when(mock_engine_client.state.labware.get_definition("abc123")).then_return( - LabwareDefinition.model_construct(ordering=[]) # type: ignore[call-arg] + LabwareDefinition2.model_construct(ordering=[]) # type: ignore[call-arg] ) result = subject.load_labware( @@ -642,15 +645,15 @@ def test_load_adapter( ) ) ).then_return( - commands.LoadLabwareResult( + commands.LoadLabwareResult.model_construct( labwareId="abc123", - definition=LabwareDefinition.model_construct(), # type: ignore[call-arg] + definition=LabwareDefinition2.model_construct(), # type: ignore[call-arg] offsetId=None, ) ) decoy.when(mock_engine_client.state.labware.get_definition("abc123")).then_return( - LabwareDefinition.model_construct(ordering=[]) # type: ignore[call-arg] + LabwareDefinition2.model_construct(ordering=[]) # type: ignore[call-arg] ) result = subject.load_adapter( @@ -714,15 +717,15 @@ def test_load_adapter_on_staging_slot( ) ) ).then_return( - commands.LoadLabwareResult( + commands.LoadLabwareResult.model_construct( labwareId="abc123", - definition=LabwareDefinition.model_construct(), # type: ignore[call-arg] + definition=LabwareDefinition2.model_construct(), # type: ignore[call-arg] offsetId=None, ) ) decoy.when(mock_engine_client.state.labware.get_definition("abc123")).then_return( - LabwareDefinition.model_construct(ordering=[]) # type: ignore[call-arg] + LabwareDefinition2.model_construct(ordering=[]) # type: ignore[call-arg] ) result = subject.load_adapter( @@ -788,14 +791,14 @@ def test_load_lid( ) ) ).then_return( - commands.LoadLidResult( + commands.LoadLidResult.model_construct( labwareId="abc123", - definition=LabwareDefinition.model_construct(ordering=[]), # type: ignore[call-arg] + definition=LabwareDefinition2.model_construct(ordering=[]), # type: ignore[call-arg] ) ) decoy.when(mock_engine_client.state.labware.get_definition("abc123")).then_return( - LabwareDefinition.model_construct(ordering=[]) # type: ignore[call-arg] + LabwareDefinition2.model_construct(ordering=[]) # type: ignore[call-arg] ) result = subject.load_lid( @@ -860,16 +863,16 @@ def test_load_lid_stack( ) ) ).then_return( - commands.LoadLidStackResult( + commands.LoadLidStackResult.model_construct( stackLabwareId="abc123", labwareIds=["1", "2", "3", "4", "5"], - definition=LabwareDefinition.model_construct(), # type: ignore[call-arg] + definition=LabwareDefinition2.model_construct(), # type: ignore[call-arg] location=DeckSlotLocation(slotName=DeckSlotName.SLOT_5), ) ) decoy.when(mock_engine_client.state.labware.get_definition("abc123")).then_return( - LabwareDefinition.model_construct(ordering=[]) # type: ignore[call-arg] + LabwareDefinition2.model_construct(ordering=[]) # type: ignore[call-arg] ) result = subject.load_lid_stack( @@ -1010,7 +1013,7 @@ def test_move_labware( decoy.when( mock_engine_client.state.labware.get_definition("labware-id") ).then_return( - LabwareDefinition.model_construct(ordering=[]) # type: ignore[call-arg] + LabwareDefinition2.model_construct(ordering=[]) # type: ignore[call-arg] ) labware = LabwareCore(labware_id="labware-id", engine_client=mock_engine_client) subject.move_labware( @@ -1053,7 +1056,7 @@ def test_move_labware_on_staging_slot( decoy.when( mock_engine_client.state.labware.get_definition("labware-id") ).then_return( - LabwareDefinition.model_construct(ordering=[]) # type: ignore[call-arg] + LabwareDefinition2.model_construct(ordering=[]) # type: ignore[call-arg] ) labware = LabwareCore(labware_id="labware-id", engine_client=mock_engine_client) subject.move_labware( @@ -1094,7 +1097,7 @@ def test_move_labware_on_non_connected_module( decoy.when( mock_engine_client.state.labware.get_definition("labware-id") ).then_return( - LabwareDefinition.model_construct(ordering=[]) # type: ignore[call-arg] + LabwareDefinition2.model_construct(ordering=[]) # type: ignore[call-arg] ) labware = LabwareCore(labware_id="labware-id", engine_client=mock_engine_client) non_connected_module_core = NonConnectedModuleCore( @@ -1140,7 +1143,7 @@ def test_move_labware_off_deck( decoy.when( mock_engine_client.state.labware.get_definition("labware-id") ).then_return( - LabwareDefinition.model_construct(ordering=[]) # type: ignore[call-arg] + LabwareDefinition2.model_construct(ordering=[]) # type: ignore[call-arg] ) labware = LabwareCore(labware_id="labware-id", engine_client=mock_engine_client) @@ -1204,15 +1207,15 @@ def test_load_labware_on_module( ) ) ).then_return( - commands.LoadLabwareResult( + commands.LoadLabwareResult.model_construct( labwareId="abc123", - definition=LabwareDefinition.model_construct(), # type: ignore[call-arg] + definition=LabwareDefinition2.model_construct(), # type: ignore[call-arg] offsetId=None, ) ) decoy.when(mock_engine_client.state.labware.get_definition("abc123")).then_return( - LabwareDefinition.model_construct(ordering=[]) # type: ignore[call-arg] + LabwareDefinition2.model_construct(ordering=[]) # type: ignore[call-arg] ) module_core = ModuleCore( @@ -1281,15 +1284,15 @@ def test_load_labware_on_non_connected_module( ) ) ).then_return( - commands.LoadLabwareResult( + commands.LoadLabwareResult.model_construct( labwareId="abc123", - definition=LabwareDefinition.model_construct(), # type: ignore[call-arg] + definition=LabwareDefinition2.model_construct(), # type: ignore[call-arg] offsetId=None, ) ) decoy.when(mock_engine_client.state.labware.get_definition("abc123")).then_return( - LabwareDefinition.model_construct(ordering=[]) # type: ignore[call-arg] + LabwareDefinition2.model_construct(ordering=[]) # type: ignore[call-arg] ) non_connected_module_core = NonConnectedModuleCore( @@ -1335,7 +1338,9 @@ def test_add_labware_definition( """It should add a labware definition to the engine.""" decoy.when( mock_engine_client.add_labware_definition( - definition=LabwareDefinition.model_validate(minimal_labware_def) + definition=labware_definition_type_adapter.validate_python( + minimal_labware_def + ) ) ).then_return(LabwareUri("hello/world/123")) diff --git a/api/tests/opentrons/protocol_api/core/engine/test_stringify.py b/api/tests/opentrons/protocol_api/core/engine/test_stringify.py index 4ccc8e5f9ba..49eff483c5f 100644 --- a/api/tests/opentrons/protocol_api/core/engine/test_stringify.py +++ b/api/tests/opentrons/protocol_api/core/engine/test_stringify.py @@ -2,7 +2,7 @@ from decoy import Decoy -from opentrons_shared_data.labware.labware_definition import LabwareDefinition +from opentrons_shared_data.labware.labware_definition import LabwareDefinition2 from opentrons.protocol_api.core.engine import stringify as subject from opentrons.protocol_engine.clients.sync_client import SyncClient @@ -18,8 +18,8 @@ def _make_dummy_labware_definition( decoy: Decoy, display_name: str -) -> LabwareDefinition: - mock = decoy.mock(cls=LabwareDefinition) +) -> LabwareDefinition2: + mock = decoy.mock(cls=LabwareDefinition2) decoy.when(mock.metadata.displayName).then_return(display_name) return mock diff --git a/api/tests/opentrons/protocol_api/core/engine/test_well_core.py b/api/tests/opentrons/protocol_api/core/engine/test_well_core.py index 41503262153..a45ba03aac2 100644 --- a/api/tests/opentrons/protocol_api/core/engine/test_well_core.py +++ b/api/tests/opentrons/protocol_api/core/engine/test_well_core.py @@ -5,7 +5,11 @@ import pytest from decoy import Decoy -from opentrons_shared_data.labware.labware_definition import WellDefinition +from opentrons_shared_data.labware.labware_definition import ( + WellDefinition2, + RectangularWellDefinition2, + CircularWellDefinition2, +) from opentrons.protocol_api import MAX_SUPPORTED_VERSION from opentrons.protocol_engine import WellLocation, WellOrigin, WellOffset @@ -57,14 +61,14 @@ def api_version() -> APIVersion: @pytest.fixture -def well_definition() -> WellDefinition: - """Get a partial WellDefinition value object.""" - return WellDefinition.model_construct() # type: ignore[call-arg] +def well_definition() -> WellDefinition2: + """Get a partial WellDefinition2 value object.""" + return CircularWellDefinition2.model_construct() # type: ignore[call-arg] @pytest.fixture def subject( - decoy: Decoy, mock_engine_client: EngineClient, well_definition: WellDefinition + decoy: Decoy, mock_engine_client: EngineClient, well_definition: WellDefinition2 ) -> WellCore: """Get a WellCore test subject with mocked dependencies.""" decoy.when( @@ -103,7 +107,7 @@ def test_display_name( @pytest.mark.parametrize( "well_definition", - [WellDefinition.model_construct(totalLiquidVolume=101)], # type: ignore[call-arg] + [CircularWellDefinition2.model_construct(totalLiquidVolume=101)], # type: ignore[call-arg] ) def test_max_volume(subject: WellCore) -> None: """It should have a max volume.""" @@ -227,7 +231,7 @@ def test_load_liquid( @pytest.mark.parametrize( "well_definition", - [WellDefinition.model_construct(diameter=123.4)], # type: ignore[call-arg] + [CircularWellDefinition2.model_construct(shape="circular", diameter=123.4)], # type: ignore[call-arg] ) def test_diameter(subject: WellCore) -> None: """It should get the diameter.""" @@ -236,7 +240,7 @@ def test_diameter(subject: WellCore) -> None: @pytest.mark.parametrize( "well_definition", - [WellDefinition.model_construct(xDimension=567.8)], # type: ignore[call-arg] + [RectangularWellDefinition2.model_construct(shape="rectangular", xDimension=567.8)], # type: ignore[call-arg] ) def test_length(subject: WellCore) -> None: """It should get the length.""" @@ -245,7 +249,7 @@ def test_length(subject: WellCore) -> None: @pytest.mark.parametrize( "well_definition", - [WellDefinition.model_construct(yDimension=987.6)], # type: ignore[call-arg] + [RectangularWellDefinition2.model_construct(shape="rectangular", yDimension=987.6)], # type: ignore[call-arg] ) def test_width(subject: WellCore) -> None: """It should get the width.""" @@ -254,7 +258,7 @@ def test_width(subject: WellCore) -> None: @pytest.mark.parametrize( "well_definition", - [WellDefinition.model_construct(depth=42.0)], # type: ignore[call-arg] + [CircularWellDefinition2.model_construct(depth=42.0)], # type: ignore[call-arg] ) def test_depth(subject: WellCore) -> None: """It should get the depth.""" diff --git a/api/tests/opentrons/protocol_api/test_validation.py b/api/tests/opentrons/protocol_api/test_validation.py index 1fb29fec6cc..6522762d6d1 100644 --- a/api/tests/opentrons/protocol_api/test_validation.py +++ b/api/tests/opentrons/protocol_api/test_validation.py @@ -9,9 +9,9 @@ from opentrons.protocols.advanced_control.transfers.common import TransferTipPolicyV2 from opentrons_shared_data.labware.labware_definition import ( - LabwareDefinition, + LabwareDefinition2, LabwareRole, - Parameters as LabwareDefinitionParameters, + Parameters2 as LabwareDefinition2Parameters, ) from opentrons_shared_data.pipette.types import PipetteNameType from opentrons_shared_data.robot.types import RobotType @@ -246,30 +246,30 @@ def test_ensure_lowercase_name_invalid() -> None: ("definition", "expected_raise"), [ ( - LabwareDefinition.model_construct( # type: ignore[call-arg] + LabwareDefinition2.model_construct( # type: ignore[call-arg] allowedRoles=[LabwareRole.labware], - parameters=LabwareDefinitionParameters.model_construct(loadName="Foo"), # type: ignore[call-arg] + parameters=LabwareDefinition2Parameters.model_construct(loadName="Foo"), # type: ignore[call-arg] ), do_not_raise(), ), ( - LabwareDefinition.model_construct( # type: ignore[call-arg] + LabwareDefinition2.model_construct( # type: ignore[call-arg] allowedRoles=[], - parameters=LabwareDefinitionParameters.model_construct(loadName="Foo"), # type: ignore[call-arg] + parameters=LabwareDefinition2Parameters.model_construct(loadName="Foo"), # type: ignore[call-arg] ), do_not_raise(), ), ( - LabwareDefinition.model_construct( # type: ignore[call-arg] + LabwareDefinition2.model_construct( # type: ignore[call-arg] allowedRoles=[LabwareRole.adapter], - parameters=LabwareDefinitionParameters.model_construct(loadName="Foo"), # type: ignore[call-arg] + parameters=LabwareDefinition2Parameters.model_construct(loadName="Foo"), # type: ignore[call-arg] ), pytest.raises(subject.LabwareDefinitionIsNotLabwareError), ), ], ) def test_ensure_definition_is_labware( - definition: LabwareDefinition, expected_raise: ContextManager[Any] + definition: LabwareDefinition2, expected_raise: ContextManager[Any] ) -> None: """It should check if the Labware Definition is defined as a regular labware.""" with expected_raise: @@ -280,30 +280,30 @@ def test_ensure_definition_is_labware( ("definition", "expected_raise"), [ ( - LabwareDefinition.model_construct( # type: ignore[call-arg] + LabwareDefinition2.model_construct( # type: ignore[call-arg] allowedRoles=[LabwareRole.adapter], - parameters=LabwareDefinitionParameters.model_construct(loadName="Foo"), # type: ignore[call-arg] + parameters=LabwareDefinition2Parameters.model_construct(loadName="Foo"), # type: ignore[call-arg] ), do_not_raise(), ), ( - LabwareDefinition.model_construct( # type: ignore[call-arg] + LabwareDefinition2.model_construct( # type: ignore[call-arg] allowedRoles=[], - parameters=LabwareDefinitionParameters.model_construct(loadName="Foo"), # type: ignore[call-arg] + parameters=LabwareDefinition2Parameters.model_construct(loadName="Foo"), # type: ignore[call-arg] ), pytest.raises(subject.LabwareDefinitionIsNotAdapterError), ), ( - LabwareDefinition.model_construct( # type: ignore[call-arg] + LabwareDefinition2.model_construct( # type: ignore[call-arg] allowedRoles=[LabwareRole.labware], - parameters=LabwareDefinitionParameters.model_construct(loadName="Foo"), # type: ignore[call-arg] + parameters=LabwareDefinition2Parameters.model_construct(loadName="Foo"), # type: ignore[call-arg] ), pytest.raises(subject.LabwareDefinitionIsNotAdapterError), ), ], ) def test_ensure_definition_is_adapter( - definition: LabwareDefinition, expected_raise: ContextManager[Any] + definition: LabwareDefinition2, expected_raise: ContextManager[Any] ) -> None: """It should check if the Labware Definition is defined as an adapter.""" with expected_raise: diff --git a/api/tests/opentrons/protocol_engine/clients/test_child_thread_transport.py b/api/tests/opentrons/protocol_engine/clients/test_child_thread_transport.py index 700f11ff190..7c22f6e4c2b 100644 --- a/api/tests/opentrons/protocol_engine/clients/test_child_thread_transport.py +++ b/api/tests/opentrons/protocol_engine/clients/test_child_thread_transport.py @@ -10,7 +10,7 @@ from decoy import Decoy from opentrons_shared_data.labware.types import LabwareUri -from opentrons_shared_data.labware.labware_definition import LabwareDefinition +from opentrons_shared_data.labware.labware_definition import LabwareDefinition2 from opentrons.protocol_engine import ProtocolEngine, commands, DeckPoint from opentrons.protocol_engine.errors import ProtocolCommandFailedError, ErrorOccurrence @@ -105,7 +105,7 @@ async def test_call_method( subject: ChildThreadTransport, ) -> None: """It should call a synchronous method in a thread-safe manner.""" - labware_def = LabwareDefinition.model_construct(namespace="hello") # type: ignore[call-arg] + labware_def = LabwareDefinition2.model_construct(namespace="hello") # type: ignore[call-arg] labware_uri = LabwareUri("hello/world/123") calling_thread_id = None diff --git a/api/tests/opentrons/protocol_engine/clients/test_sync_client.py b/api/tests/opentrons/protocol_engine/clients/test_sync_client.py index 628e23cc052..e8b0c61deae 100644 --- a/api/tests/opentrons/protocol_engine/clients/test_sync_client.py +++ b/api/tests/opentrons/protocol_engine/clients/test_sync_client.py @@ -14,7 +14,7 @@ from opentrons_shared_data.labware.types import LabwareUri -from opentrons_shared_data.labware.labware_definition import LabwareDefinition +from opentrons_shared_data.labware.labware_definition import LabwareDefinition2 from opentrons.protocol_engine import commands from opentrons.protocol_engine.clients import SyncClient, ChildThreadTransport @@ -71,7 +71,7 @@ def test_add_labware_definition( subject: SyncClient, ) -> None: """It should add a labware definition.""" - labware_definition = LabwareDefinition.model_construct(namespace="hello") # type: ignore[call-arg] + labware_definition = LabwareDefinition2.model_construct(namespace="hello") # type: ignore[call-arg] expected_labware_uri = LabwareUri("hello/world/123") decoy.when( diff --git a/api/tests/opentrons/protocol_engine/commands/absorbance_reader/test_close_lid.py b/api/tests/opentrons/protocol_engine/commands/absorbance_reader/test_close_lid.py index c40a825d9c6..65ad1e2cfc6 100644 --- a/api/tests/opentrons/protocol_engine/commands/absorbance_reader/test_close_lid.py +++ b/api/tests/opentrons/protocol_engine/commands/absorbance_reader/test_close_lid.py @@ -30,18 +30,18 @@ ) from opentrons.types import DeckSlotName from opentrons_shared_data.labware.labware_definition import ( - LabwareDefinition, - Parameters, + LabwareDefinition2, + Parameters2, ) @pytest.fixture -def absorbance_def() -> LabwareDefinition: +def absorbance_def() -> LabwareDefinition2: """Get a tip rack Pydantic model definition value object.""" - return LabwareDefinition.model_construct( # type: ignore[call-arg] + return LabwareDefinition2.model_construct( # type: ignore[call-arg] namespace="test", version=1, - parameters=Parameters.model_construct( # type: ignore[call-arg] + parameters=Parameters2.model_construct( # type: ignore[call-arg] loadName="cool-labware", tipOverlap=None, # add a None value to validate serialization to dictionary ), @@ -70,7 +70,7 @@ async def test_absorbance_reader_close_lid_implementation( state_view: StateView, equipment: EquipmentHandler, hardware_lid_status: AbsorbanceReaderLidStatus, - absorbance_def: LabwareDefinition, + absorbance_def: LabwareDefinition2, ) -> None: """It should validate, find hardware module if not virtualized, and close lid.""" params = CloseLidParams( @@ -169,7 +169,7 @@ async def test_close_lid_raises_no_gripper_offset( state_view: StateView, equipment: EquipmentHandler, subject: CloseLidImpl, - absorbance_def: LabwareDefinition, + absorbance_def: LabwareDefinition2, ) -> None: """Should raise an error that gripper offset not found.""" params = CloseLidParams( diff --git a/api/tests/opentrons/protocol_engine/commands/absorbance_reader/test_open_lid.py b/api/tests/opentrons/protocol_engine/commands/absorbance_reader/test_open_lid.py index bc555a9bb18..186276bc30f 100644 --- a/api/tests/opentrons/protocol_engine/commands/absorbance_reader/test_open_lid.py +++ b/api/tests/opentrons/protocol_engine/commands/absorbance_reader/test_open_lid.py @@ -30,18 +30,18 @@ ) from opentrons.types import DeckSlotName from opentrons_shared_data.labware.labware_definition import ( - LabwareDefinition, - Parameters, + LabwareDefinition2, + Parameters2, ) @pytest.fixture -def absorbance_def() -> LabwareDefinition: +def absorbance_def() -> LabwareDefinition2: """Get a tip rack Pydantic model definition value object.""" - return LabwareDefinition.model_construct( # type: ignore[call-arg] + return LabwareDefinition2.model_construct( # type: ignore[call-arg] namespace="test", version=1, - parameters=Parameters.model_construct( # type: ignore[call-arg] + parameters=Parameters2.model_construct( # type: ignore[call-arg] loadName="cool-labware", tipOverlap=None, # add a None value to validate serialization to dictionary ), @@ -70,7 +70,7 @@ async def test_absorbance_reader_implementation( equipment: EquipmentHandler, labware_movement: LabwareMovementHandler, hardware_lid_status: AbsorbanceReaderLidStatus, - absorbance_def: LabwareDefinition, + absorbance_def: LabwareDefinition2, subject: OpenLidImpl, ) -> None: """It should validate, find hardware module if not virtualized, and disengage.""" @@ -170,7 +170,7 @@ async def test_open_lid_raises_no_gripper_offset( state_view: StateView, equipment: EquipmentHandler, subject: OpenLidImpl, - absorbance_def: LabwareDefinition, + absorbance_def: LabwareDefinition2, ) -> None: """Should raise an error that gripper offset not found.""" params = OpenLidParams( diff --git a/api/tests/opentrons/protocol_engine/commands/test_evotip_dispense.py b/api/tests/opentrons/protocol_engine/commands/test_evotip_dispense.py index 2f9a27cf0d7..8e415989bac 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_evotip_dispense.py +++ b/api/tests/opentrons/protocol_engine/commands/test_evotip_dispense.py @@ -16,7 +16,10 @@ MovementHandler, ) -from opentrons_shared_data.labware.labware_definition import LabwareDefinition +from opentrons_shared_data.labware.labware_definition import ( + LabwareDefinition, + labware_definition_type_adapter, +) from opentrons.protocol_engine.commands.command import SuccessData from opentrons.protocol_engine.commands.evotip_dispense import ( EvotipDispenseParams, @@ -34,7 +37,7 @@ def evotips_definition() -> LabwareDefinition: """A fixturee of the evotips definition.""" # TODO (chb 2025-01-29): When we migrate all labware to v3 we can clean this up - return LabwareDefinition.model_validate( + return labware_definition_type_adapter.validate_python( load_definition("evotips_opentrons_96_labware", 1) ) diff --git a/api/tests/opentrons/protocol_engine/commands/test_evotip_seal_pipette.py b/api/tests/opentrons/protocol_engine/commands/test_evotip_seal_pipette.py index 59e7e3ca748..fce5cc58aaf 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_evotip_seal_pipette.py +++ b/api/tests/opentrons/protocol_engine/commands/test_evotip_seal_pipette.py @@ -6,7 +6,10 @@ from decoy import Decoy, matchers from unittest.mock import sentinel -from opentrons_shared_data.labware.labware_definition import LabwareDefinition +from opentrons_shared_data.labware.labware_definition import ( + LabwareDefinition, + labware_definition_type_adapter, +) from opentrons_shared_data.errors.exceptions import StallOrCollisionDetectedError @@ -44,7 +47,7 @@ def evotips_definition() -> LabwareDefinition: """A fixturee of the evotips definition.""" # TODO (chb 2025-01-29): When we migrate all labware to v3 we can clean this up - return LabwareDefinition.model_validate( + return labware_definition_type_adapter.validate_python( load_definition("evotips_opentrons_96_labware", 1) ) diff --git a/api/tests/opentrons/protocol_engine/commands/test_evotip_unseal_pipette.py b/api/tests/opentrons/protocol_engine/commands/test_evotip_unseal_pipette.py index ce617690bd9..509aaeb482f 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_evotip_unseal_pipette.py +++ b/api/tests/opentrons/protocol_engine/commands/test_evotip_unseal_pipette.py @@ -28,7 +28,10 @@ from opentrons.protocol_engine.execution import MovementHandler, GantryMover, TipHandler from opentrons_shared_data.labware import load_definition -from opentrons_shared_data.labware.labware_definition import LabwareDefinition +from opentrons_shared_data.labware.labware_definition import ( + LabwareDefinition, + labware_definition_type_adapter, +) from opentrons.types import Point @@ -88,7 +91,7 @@ def test_drop_tip_params_default_origin() -> None: def evotips_definition() -> LabwareDefinition: """A fixturee of the evotips definition.""" # TODO (chb 2025-01-29): When we migrate all labware to v3 we can clean this up - return LabwareDefinition.model_validate( + return labware_definition_type_adapter.validate_python( load_definition("evotips_opentrons_96_labware", 1) ) diff --git a/api/tests/opentrons/protocol_engine/commands/test_move_labware.py b/api/tests/opentrons/protocol_engine/commands/test_move_labware.py index fe7756498fe..da7b91dc1e6 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_move_labware.py +++ b/api/tests/opentrons/protocol_engine/commands/test_move_labware.py @@ -8,8 +8,8 @@ from decoy import Decoy, matchers from opentrons_shared_data.labware.labware_definition import ( - LabwareDefinition, - Parameters, + LabwareDefinition2, + Parameters2, Dimensions, ) from opentrons_shared_data.errors.exceptions import ( @@ -194,7 +194,7 @@ async def test_move_labware_implementation_on_labware( decoy.when( state_view.labware.get_definition(labware_id="my-cool-labware-id") ).then_return( - LabwareDefinition.model_construct(namespace="spacename") # type: ignore[call-arg] + LabwareDefinition2.model_construct(namespace="spacename") # type: ignore[call-arg] ) decoy.when( state_view.geometry.ensure_location_not_occupied( @@ -230,7 +230,7 @@ async def test_move_labware_implementation_on_labware( "my-even-cooler-labware-id" ), state_view.labware.raise_if_labware_cannot_be_stacked( - LabwareDefinition.model_construct(namespace="spacename"), # type: ignore[call-arg] + LabwareDefinition2.model_construct(namespace="spacename"), # type: ignore[call-arg] "my-even-cooler-labware-id", ), ) @@ -395,7 +395,7 @@ async def test_gripper_error( labware_namespace = "labware-namespace" labware_load_name = "load-name" labware_definition_uri = "opentrons-test/load-name/1" - labware_def = LabwareDefinition.model_construct( # type: ignore[call-arg] + labware_def = LabwareDefinition2.model_construct( # type: ignore[call-arg] namespace=labware_namespace, ) origin_location = DeckSlotLocation(slotName=DeckSlotName.SLOT_A1) @@ -405,7 +405,7 @@ async def test_gripper_error( # Common MoveLabwareImplementation boilerplate: decoy.when(state_view.labware.get_definition(labware_id=labware_id)).then_return( - LabwareDefinition.model_construct(namespace=labware_namespace) # type: ignore[call-arg] + LabwareDefinition2.model_construct(namespace=labware_namespace) # type: ignore[call-arg] ) decoy.when(state_view.labware.get(labware_id=labware_id)).then_return( LoadedLabware( @@ -579,7 +579,7 @@ async def test_gripper_move_to_waste_chute_implementation( pickUpOffset=LabwareOffsetVector(x=1, y=2, z=3), dropOffset=None, ) - labware_def = LabwareDefinition.model_construct( # type: ignore[call-arg] + labware_def = LabwareDefinition2.model_construct( # type: ignore[call-arg] namespace="my-cool-namespace", dimensions=Dimensions( yDimension=labware_width, zDimension=labware_width, xDimension=labware_width @@ -798,8 +798,8 @@ async def test_move_labware_raises_when_moving_adapter_with_gripper( strategy=LabwareMovementStrategy.USING_GRIPPER, ) - definition = LabwareDefinition.model_construct( # type: ignore[call-arg] - parameters=Parameters.model_construct(loadName="My cool adapter"), # type: ignore[call-arg] + definition = LabwareDefinition2.model_construct( # type: ignore[call-arg] + parameters=Parameters2.model_construct(loadName="My cool adapter"), # type: ignore[call-arg] ) decoy.when(state_view.labware.get(labware_id="my-cool-labware-id")).then_return( @@ -839,8 +839,8 @@ async def test_move_labware_raises_when_moving_labware_with_gripper_incompatible strategy=LabwareMovementStrategy.USING_GRIPPER, ) - definition = LabwareDefinition.model_construct( # type: ignore[call-arg] - parameters=Parameters.model_construct(loadName="My cool labware"), # type: ignore[call-arg] + definition = LabwareDefinition2.model_construct( # type: ignore[call-arg] + parameters=Parameters2.model_construct(loadName="My cool labware"), # type: ignore[call-arg] ) decoy.when(state_view.labware.get(labware_id="my-cool-labware-id")).then_return( @@ -889,7 +889,7 @@ async def test_move_labware_with_gripper_raises_on_ot2( decoy.when( state_view.labware.get_definition(labware_id="my-cool-labware-id") ).then_return( - LabwareDefinition.model_construct(namespace="spacename") # type: ignore[call-arg] + LabwareDefinition2.model_construct(namespace="spacename") # type: ignore[call-arg] ) decoy.when(state_view.config).then_return( @@ -911,8 +911,8 @@ async def test_move_labware_raises_when_moving_fixed_trash_labware( strategy=LabwareMovementStrategy.USING_GRIPPER, ) - definition = LabwareDefinition.model_construct( # type: ignore[call-arg] - parameters=Parameters.model_construct( # type: ignore[call-arg] + definition = LabwareDefinition2.model_construct( # type: ignore[call-arg] + parameters=Parameters2.model_construct( # type: ignore[call-arg] loadName="My cool labware", quirks=["fixedTrash"] ), ) diff --git a/api/tests/opentrons/protocol_engine/conftest.py b/api/tests/opentrons/protocol_engine/conftest.py index 2a6ee664d87..736bfe38a6e 100644 --- a/api/tests/opentrons/protocol_engine/conftest.py +++ b/api/tests/opentrons/protocol_engine/conftest.py @@ -10,7 +10,10 @@ from opentrons_shared_data.deck import load as load_deck from opentrons_shared_data.deck.types import DeckDefinitionV5 from opentrons_shared_data.labware import load_definition -from opentrons_shared_data.labware.labware_definition import LabwareDefinition +from opentrons_shared_data.labware.labware_definition import ( + LabwareDefinition, + labware_definition_type_adapter, +) from opentrons_shared_data.pipette import pipette_definition from opentrons.protocols.api_support.deck_type import ( STANDARD_OT2_DECK, @@ -79,7 +82,7 @@ def ot3_standard_deck_def() -> DeckDefinitionV5: @pytest.fixture(scope="session") def ot2_fixed_trash_def() -> LabwareDefinition: """Get the definition of the OT-2 standard fixed trash.""" - return LabwareDefinition.model_validate( + return labware_definition_type_adapter.validate_python( load_definition("opentrons_1_trash_1100ml_fixed", 1) ) @@ -87,7 +90,7 @@ def ot2_fixed_trash_def() -> LabwareDefinition: @pytest.fixture(scope="session") def ot2_short_fixed_trash_def() -> LabwareDefinition: """Get the definition of the OT-2 short fixed trash.""" - return LabwareDefinition.model_validate( + return labware_definition_type_adapter.validate_python( load_definition("opentrons_1_trash_850ml_fixed", 1) ) @@ -95,7 +98,7 @@ def ot2_short_fixed_trash_def() -> LabwareDefinition: @pytest.fixture(scope="session") def ot3_fixed_trash_def() -> LabwareDefinition: """Get the definition of the OT-3 fixed trash.""" - return LabwareDefinition.model_validate( + return labware_definition_type_adapter.validate_python( load_definition("opentrons_1_trash_3200ml_fixed", 1) ) @@ -103,7 +106,7 @@ def ot3_fixed_trash_def() -> LabwareDefinition: @pytest.fixture(scope="session") def ot3_absorbance_reader_lid() -> LabwareDefinition: """Get the definition of the OT-3 plate reader lid.""" - return LabwareDefinition.model_validate( + return labware_definition_type_adapter.validate_python( load_definition("opentrons_flex_lid_absorbance_plate_reader_module", 1) ) @@ -111,7 +114,7 @@ def ot3_absorbance_reader_lid() -> LabwareDefinition: @pytest.fixture(scope="session") def well_plate_def() -> LabwareDefinition: """Get the definition of a 96 well plate.""" - return LabwareDefinition.model_validate( + return labware_definition_type_adapter.validate_python( load_definition("corning_96_wellplate_360ul_flat", 2) ) @@ -119,7 +122,7 @@ def well_plate_def() -> LabwareDefinition: @pytest.fixture(scope="session") def flex_50uL_tiprack() -> LabwareDefinition: """Get the definition of a Flex 50uL tiprack.""" - return LabwareDefinition.model_validate( + return labware_definition_type_adapter.validate_python( load_definition("opentrons_flex_96_filtertiprack_50ul", 1) ) @@ -127,7 +130,7 @@ def flex_50uL_tiprack() -> LabwareDefinition: @pytest.fixture(scope="session") def adapter_plate_def() -> LabwareDefinition: """Get the definition of a h/s adapter plate.""" - return LabwareDefinition.model_validate( + return labware_definition_type_adapter.validate_python( load_definition("opentrons_universal_flat_adapter", 1) ) @@ -135,7 +138,7 @@ def adapter_plate_def() -> LabwareDefinition: @pytest.fixture(scope="session") def reservoir_def() -> LabwareDefinition: """Get the definition of single-row reservoir.""" - return LabwareDefinition.model_validate( + return labware_definition_type_adapter.validate_python( load_definition("nest_12_reservoir_15ml", 1) ) @@ -143,7 +146,7 @@ def reservoir_def() -> LabwareDefinition: @pytest.fixture(scope="session") def tip_rack_def() -> LabwareDefinition: """Get the definition of Opentrons 300 uL tip rack.""" - return LabwareDefinition.model_validate( + return labware_definition_type_adapter.validate_python( load_definition("opentrons_96_tiprack_300ul", 1) ) @@ -151,7 +154,7 @@ def tip_rack_def() -> LabwareDefinition: @pytest.fixture(scope="session") def adapter_def() -> LabwareDefinition: """Get the definition of Opentrons 96 PCR adapter.""" - return LabwareDefinition.model_validate( + return labware_definition_type_adapter.validate_python( load_definition("opentrons_96_pcr_adapter", 1) ) @@ -159,7 +162,7 @@ def adapter_def() -> LabwareDefinition: @pytest.fixture(scope="session") def lid_stack_def() -> LabwareDefinition: """Get the definition of the opentrons tiprack lid.""" - return LabwareDefinition.model_validate( + return labware_definition_type_adapter.validate_python( load_definition("protocol_engine_lid_stack_object", 1, schema=3) ) @@ -167,7 +170,7 @@ def lid_stack_def() -> LabwareDefinition: @pytest.fixture(scope="session") def falcon_tuberack_def() -> LabwareDefinition: """Get the definition of the 6-well Falcon tuberack.""" - return LabwareDefinition.model_validate( + return labware_definition_type_adapter.validate_python( load_definition("opentrons_6_tuberack_falcon_50ml_conical", 1) ) @@ -175,7 +178,7 @@ def falcon_tuberack_def() -> LabwareDefinition: @pytest.fixture(scope="session") def magdeck_well_plate_def() -> LabwareDefinition: """Get the definition of a well place compatible with magdeck.""" - return LabwareDefinition.model_validate( + return labware_definition_type_adapter.validate_python( load_definition("nest_96_wellplate_100ul_pcr_full_skirt", 1) ) @@ -183,7 +186,7 @@ def magdeck_well_plate_def() -> LabwareDefinition: @pytest.fixture(scope="session") def tiprack_lid_def() -> LabwareDefinition: """Get the definition of the opentrons tiprack lid.""" - return LabwareDefinition.model_validate( + return labware_definition_type_adapter.validate_python( load_definition("opentrons_flex_tiprack_lid", 1, schema=3) ) diff --git a/api/tests/opentrons/protocol_engine/execution/test_tip_handler.py b/api/tests/opentrons/protocol_engine/execution/test_tip_handler.py index 1f87d0cced2..998486fc489 100644 --- a/api/tests/opentrons/protocol_engine/execution/test_tip_handler.py +++ b/api/tests/opentrons/protocol_engine/execution/test_tip_handler.py @@ -19,7 +19,10 @@ CommandPreconditionViolated, CommandParameterLimitViolated, ) -from opentrons_shared_data.labware.labware_definition import LabwareDefinition +from opentrons_shared_data.labware.labware_definition import ( + LabwareDefinition, + LabwareDefinition2, +) from opentrons.protocol_engine.execution.tip_handler import ( HardwareTipHandler, VirtualTipHandler, @@ -52,7 +55,7 @@ def mock_labware_data_provider(decoy: Decoy) -> LabwareDataProvider: @pytest.fixture def tip_rack_definition() -> LabwareDefinition: """Get a tip rack defintion value object.""" - return LabwareDefinition.model_construct(namespace="test", version=42) # type: ignore[call-arg] + return LabwareDefinition2.model_construct(namespace="test", version=42) # type: ignore[call-arg] MOCK_MAP = NozzleMap.build( diff --git a/api/tests/opentrons/protocol_engine/resources/test_labware_data_provider.py b/api/tests/opentrons/protocol_engine/resources/test_labware_data_provider.py index 80c9775ad2d..ab6adef2001 100644 --- a/api/tests/opentrons/protocol_engine/resources/test_labware_data_provider.py +++ b/api/tests/opentrons/protocol_engine/resources/test_labware_data_provider.py @@ -1,7 +1,9 @@ """Functional tests for the LabwareDataProvider.""" from typing import cast -from opentrons_shared_data.labware.labware_definition import LabwareDefinition +from opentrons_shared_data.labware.labware_definition import ( + labware_definition_type_adapter, +) from opentrons_shared_data.labware.types import LabwareDefinition as LabwareDefDict from opentrons.calibration_storage.helpers import hash_labware_def from opentrons.protocol_api.labware import get_labware_definition @@ -22,7 +24,7 @@ async def test_labware_data_gets_standard_definition() -> None: version=1, ) - assert result == LabwareDefinition.model_validate(expected) + assert result == labware_definition_type_adapter.validate_python(expected) async def test_labware_hash_match() -> None: @@ -38,7 +40,7 @@ async def test_labware_hash_match() -> None: version=1, ) - labware_model = LabwareDefinition.model_validate(labware_dict) + labware_model = labware_definition_type_adapter.validate_python(labware_dict) labware_model_dict = cast( LabwareDefDict, labware_model.model_dump(exclude_none=True, exclude_unset=True) ) diff --git a/api/tests/opentrons/protocol_engine/resources/test_labware_validation.py b/api/tests/opentrons/protocol_engine/resources/test_labware_validation.py index f48ee890803..e0390956302 100644 --- a/api/tests/opentrons/protocol_engine/resources/test_labware_validation.py +++ b/api/tests/opentrons/protocol_engine/resources/test_labware_validation.py @@ -3,9 +3,10 @@ from opentrons_shared_data.labware.labware_definition import ( LabwareDefinition, + LabwareDefinition2, LabwareRole, Vector, - Parameters, + Parameters2, ) from opentrons.protocol_engine.resources import labware_validation as subject @@ -15,15 +16,15 @@ ("definition", "expected_result"), [ ( - LabwareDefinition.model_construct(allowedRoles=[LabwareRole.labware]), # type: ignore[call-arg] + LabwareDefinition2.model_construct(allowedRoles=[LabwareRole.labware]), # type: ignore[call-arg] True, ), ( - LabwareDefinition.model_construct(allowedRoles=[]), # type: ignore[call-arg] + LabwareDefinition2.model_construct(allowedRoles=[]), # type: ignore[call-arg] True, ), ( - LabwareDefinition.model_construct(allowedRoles=[LabwareRole.adapter]), # type: ignore[call-arg] + LabwareDefinition2.model_construct(allowedRoles=[LabwareRole.adapter]), # type: ignore[call-arg] False, ), ], @@ -39,15 +40,15 @@ def test_validate_definition_is_labware( ("definition", "expected_result"), [ ( - LabwareDefinition.model_construct(allowedRoles=[LabwareRole.adapter]), # type: ignore[call-arg] + LabwareDefinition2.model_construct(allowedRoles=[LabwareRole.adapter]), # type: ignore[call-arg] True, ), ( - LabwareDefinition.model_construct(allowedRoles=[]), # type: ignore[call-arg] + LabwareDefinition2.model_construct(allowedRoles=[]), # type: ignore[call-arg] False, ), ( - LabwareDefinition.model_construct(allowedRoles=[LabwareRole.labware]), # type: ignore[call-arg] + LabwareDefinition2.model_construct(allowedRoles=[LabwareRole.labware]), # type: ignore[call-arg] False, ), ], @@ -63,19 +64,19 @@ def test_validate_definition_is_adapter( ("definition", "expected_result"), [ ( - LabwareDefinition.model_construct( # type: ignore[call-arg] + LabwareDefinition2.model_construct( # type: ignore[call-arg] stackingOffsetWithLabware={"labware123": Vector(x=4, y=5, z=6)} ), True, ), ( - LabwareDefinition.model_construct( # type: ignore[call-arg] + LabwareDefinition2.model_construct( # type: ignore[call-arg] stackingOffsetWithLabware={"labwareXYZ": Vector(x=4, y=5, z=6)} ), False, ), ( - LabwareDefinition.model_construct(stackingOffsetWithLabware={}), # type: ignore[call-arg] + LabwareDefinition2.model_construct(stackingOffsetWithLabware={}), # type: ignore[call-arg] False, ), ], @@ -94,20 +95,20 @@ def test_validate_labware_can_be_stacked( ("definition", "expected_result"), [ ( - LabwareDefinition.model_construct( # type: ignore[call-arg] - parameters=Parameters.model_construct(quirks=None) # type: ignore[call-arg] + LabwareDefinition2.model_construct( # type: ignore[call-arg] + parameters=Parameters2.model_construct(quirks=None) # type: ignore[call-arg] ), True, ), ( - LabwareDefinition.model_construct( # type: ignore[call-arg] - parameters=Parameters.model_construct(quirks=["foo"]) # type: ignore[call-arg] + LabwareDefinition2.model_construct( # type: ignore[call-arg] + parameters=Parameters2.model_construct(quirks=["foo"]) # type: ignore[call-arg] ), True, ), ( - LabwareDefinition.model_construct( # type: ignore[call-arg] - parameters=Parameters.model_construct(quirks=["gripperIncompatible"]) # type: ignore[call-arg] + LabwareDefinition2.model_construct( # type: ignore[call-arg] + parameters=Parameters2.model_construct(quirks=["gripperIncompatible"]) # type: ignore[call-arg] ), False, ), diff --git a/api/tests/opentrons/protocol_engine/state/test_geometry_view.py b/api/tests/opentrons/protocol_engine/state/test_geometry_view.py index 4be6954cbaf..24131e31c16 100644 --- a/api/tests/opentrons/protocol_engine/state/test_geometry_view.py +++ b/api/tests/opentrons/protocol_engine/state/test_geometry_view.py @@ -22,17 +22,23 @@ from opentrons_shared_data import get_shared_data_root, load_shared_data from opentrons_shared_data.deck.types import DeckDefinitionV5 from opentrons_shared_data.deck import load as load_deck -from opentrons_shared_data.labware.labware_definition import LabwareDefinition from opentrons_shared_data.labware.types import LabwareUri from opentrons_shared_data.pipette import pipette_definition from opentrons.calibration_storage.helpers import uri_from_details from opentrons.types import Point, DeckSlotName, MountType, StagingSlotName from opentrons_shared_data.pipette.types import PipetteNameType from opentrons_shared_data.labware.labware_definition import ( + CuboidalFrustum, + InnerWellGeometry, + LabwareDefinition, + LabwareDefinition2, Dimensions as LabwareDimensions, - Parameters as LabwareDefinitionParameters, + Parameters2 as LabwareDefinition2Parameters, + RectangularWellDefinition3, + SphericalSegment, Vector as LabwareDefinitionVector, ConicalFrustum, + labware_definition_type_adapter, ) from opentrons_shared_data.labware import load_definition as load_labware_definition @@ -116,7 +122,36 @@ from ..pipette_fixtures import get_default_nozzle_map from ..mock_circular_frusta import TEST_EXAMPLES as CIRCULAR_TEST_EXAMPLES from ..mock_rectangular_frusta import TEST_EXAMPLES as RECTANGULAR_TEST_EXAMPLES -from ...protocol_runner.test_json_translator import _load_labware_definition_data + + +_TEST_INNER_WELL_GEOMETRY = InnerWellGeometry( + sections=[ + CuboidalFrustum( + shape="cuboidal", + topXDimension=7.6, + topYDimension=8.5, + bottomXDimension=5.6, + bottomYDimension=6.5, + topHeight=45, + bottomHeight=20, + ), + CuboidalFrustum( + shape="cuboidal", + topXDimension=5.6, + topYDimension=6.5, + bottomXDimension=4.5, + bottomYDimension=4.0, + topHeight=20, + bottomHeight=10, + ), + SphericalSegment( + shape="spherical", + radiusOfCurvature=6, + topHeight=10, + bottomHeight=0.0, + ), + ], +) @pytest.fixture @@ -275,7 +310,7 @@ def addressable_area_view( @pytest.fixture def nice_labware_definition() -> LabwareDefinition: """Load a nice labware def that won't blow up your terminal.""" - return LabwareDefinition.model_validate( + return labware_definition_type_adapter.validate_python( json.loads( load_shared_data("labware/fixtures/2/fixture_12_trough_v2.json").decode( "utf-8" @@ -287,7 +322,7 @@ def nice_labware_definition() -> LabwareDefinition: @pytest.fixture def nice_adapter_definition() -> LabwareDefinition: """Load a friendly adapter definition.""" - return LabwareDefinition.model_validate( + return labware_definition_type_adapter.validate_python( json.loads( load_shared_data( "labware/definitions/2/opentrons_aluminum_flat_bottom_plate/1.json" @@ -1734,11 +1769,8 @@ def test_get_well_position_with_meniscus_and_literal_volume_offset( probed_volume=None, ) ) - labware_def = _load_labware_definition_data() - assert labware_def.innerLabwareGeometry is not None - inner_well_def = labware_def.innerLabwareGeometry["welldefinition1111"] decoy.when(mock_labware_view.get_well_geometry("labware-id", "B2")).then_return( - inner_well_def + _TEST_INNER_WELL_GEOMETRY ) decoy.when( mock_pipette_view.get_current_tip_lld_settings(pipette_id="pipette-id") @@ -1808,11 +1840,8 @@ def test_get_well_position_with_meniscus_and_float_volume_offset( probed_volume=None, ) ) - labware_def = _load_labware_definition_data() - assert labware_def.innerLabwareGeometry is not None - inner_well_def = labware_def.innerLabwareGeometry["welldefinition1111"] decoy.when(mock_labware_view.get_well_geometry("labware-id", "B2")).then_return( - inner_well_def + _TEST_INNER_WELL_GEOMETRY ) decoy.when( mock_pipette_view.get_current_tip_lld_settings(pipette_id="pipette-id") @@ -1881,11 +1910,8 @@ def test_get_well_position_raises_validation_error( probed_volume=None, ) ) - labware_def = _load_labware_definition_data() - assert labware_def.innerLabwareGeometry is not None - inner_well_def = labware_def.innerLabwareGeometry["welldefinition1111"] decoy.when(mock_labware_view.get_well_geometry("labware-id", "B2")).then_return( - inner_well_def + _TEST_INNER_WELL_GEOMETRY ) decoy.when( mock_pipette_view.get_current_tip_lld_settings(pipette_id="pipette-id") @@ -1952,11 +1978,8 @@ def test_get_meniscus_height( probed_volume=None, ) ) - labware_def = _load_labware_definition_data() - assert labware_def.innerLabwareGeometry is not None - inner_well_def = labware_def.innerLabwareGeometry["welldefinition1111"] decoy.when(mock_labware_view.get_well_geometry("labware-id", "B2")).then_return( - inner_well_def + _TEST_INNER_WELL_GEOMETRY ) decoy.when( mock_pipette_view.get_current_tip_lld_settings(pipette_id="pipette-id") @@ -2123,6 +2146,7 @@ def test_get_nominal_tip_geometry( ) assert result.length == 100 + assert well_def.shape == "circular" # For type checking, required for `.diameter`. assert result.diameter == well_def.diameter assert result.volume == well_def.totalLiquidVolume @@ -2943,13 +2967,13 @@ def test_check_gripper_labware_tip_collision( ) ) - definition = LabwareDefinition.model_construct( # type: ignore[call-arg] + definition = LabwareDefinition2.model_construct( # type: ignore[call-arg] namespace="hello", dimensions=LabwareDimensions.model_construct( yDimension=1, zDimension=2, xDimension=3 ), version=1, - parameters=LabwareDefinitionParameters.model_construct( + parameters=LabwareDefinition2Parameters.model_construct( format="96Standard", loadName="labware-id", isTiprack=True, @@ -3383,17 +3407,11 @@ def test_validate_dispense_volume_into_well_meniscus( subject: GeometryView, ) -> None: """It should raise an InvalidDispenseVolumeError if too much volume is specified.""" - labware_def = _load_labware_definition_data() - assert labware_def.wells is not None - well_def = labware_def.wells["A1"] - assert labware_def.innerLabwareGeometry is not None - inner_well_def = labware_def.innerLabwareGeometry["welldefinition1111"] - decoy.when(mock_labware_view.get_well_definition("labware-id", "A1")).then_return( - well_def + RectangularWellDefinition3.model_construct(totalLiquidVolume=1100000) # type: ignore[call-arg] ) decoy.when(mock_labware_view.get_well_geometry("labware-id", "A1")).then_return( - inner_well_def + _TEST_INNER_WELL_GEOMETRY ) probe_time = datetime.now() decoy.when(mock_well_view.get_last_liquid_update("labware-id", "A1")).then_return( @@ -3427,20 +3445,11 @@ def test_get_latest_volume_information( ) -> None: """It should raise an InvalidDispenseVolumeError if too much volume is specified.""" # Setup - labware_def = _load_labware_definition_data() - assert labware_def.wells is not None - well_def = labware_def.wells["A1"] - assert labware_def.innerLabwareGeometry is not None - inner_well_def = labware_def.innerLabwareGeometry["welldefinition1111"] - load_time = datetime.min probe_time = datetime.now() - decoy.when(mock_labware_view.get_well_definition("labware-id", "A1")).then_return( - well_def - ) decoy.when(mock_labware_view.get_well_geometry("labware-id", "A1")).then_return( - inner_well_def + _TEST_INNER_WELL_GEOMETRY ) ten_ul_height = subject.get_well_height_at_volume( labware_id="labware-id", well_name="A1", volume=10.0 @@ -3570,13 +3579,15 @@ def _get_labware_def() -> LabwareDefinition: def_dir = str(get_shared_data_root()) + f"/labware/definitions/3/{labware_id}" version_str = max([str(version) for version in listdir(def_dir)]) def_path = path.join(def_dir, version_str) - _labware_def = LabwareDefinition.model_validate( + _labware_def = labware_definition_type_adapter.validate_python( json.loads(load_shared_data(def_path).decode("utf-8")) ) return _labware_def labware_def = _get_labware_def() - assert labware_def.innerLabwareGeometry is not None + assert ( + labware_def.schemaVersion == 3 and labware_def.innerLabwareGeometry is not None + ) well_geometry = labware_def.innerLabwareGeometry.get(well_name) assert well_geometry is not None well_definition = [ @@ -3634,13 +3645,15 @@ def _get_labware_def() -> LabwareDefinition: def_dir = str(get_shared_data_root()) + f"/labware/definitions/3/{labware_id}" version_str = max([str(version) for version in listdir(def_dir)]) def_path = path.join(def_dir, version_str) - _labware_def = LabwareDefinition.model_validate( + _labware_def = labware_definition_type_adapter.validate_python( json.loads(load_shared_data(def_path).decode("utf-8")) ) return _labware_def labware_def = _get_labware_def() - assert labware_def.innerLabwareGeometry is not None + assert ( + labware_def.schemaVersion == 3 and labware_def.innerLabwareGeometry is not None + ) well_geometry = labware_def.innerLabwareGeometry.get(well_name) assert well_geometry is not None well_definition = [ @@ -4042,7 +4055,7 @@ def test_get_location_sequence_stacker_hopper( pytest.param([], 0, id="empty-list"), pytest.param( [ - LabwareDefinition.model_validate( + labware_definition_type_adapter.validate_python( load_labware_definition( "corning_96_wellplate_360ul_flat", version=2 ) @@ -4053,12 +4066,12 @@ def test_get_location_sequence_stacker_hopper( ), pytest.param( [ - LabwareDefinition.model_validate( + labware_definition_type_adapter.validate_python( load_labware_definition( "opentrons_flex_tiprack_lid", version=1, schema=3 ) ), - LabwareDefinition.model_validate( + labware_definition_type_adapter.validate_python( load_labware_definition( "opentrons_flex_96_tiprack_1000ul", version=1 ) diff --git a/api/tests/opentrons/protocol_engine/state/test_labware_view_old.py b/api/tests/opentrons/protocol_engine/state/test_labware_view_old.py index c5ed359e63f..446dfb672c5 100644 --- a/api/tests/opentrons/protocol_engine/state/test_labware_view_old.py +++ b/api/tests/opentrons/protocol_engine/state/test_labware_view_old.py @@ -15,11 +15,13 @@ from opentrons_shared_data.pipette.types import LabwareUri from opentrons_shared_data.labware import load_definition from opentrons_shared_data.labware.labware_definition import ( - Parameters, + Parameters2, LabwareDefinition, + LabwareDefinition2, LabwareRole, GripperOffsets, Vector, + labware_definition_type_adapter, ) from opentrons.protocols.api_support.deck_type import ( @@ -324,13 +326,13 @@ def test_find_custom_labware_params( namespace: Optional[str], version: Optional[int] ) -> None: """It should find the missing (if any) load labware parameters.""" - labware_def = LabwareDefinition.model_construct( # type: ignore[call-arg] - parameters=Parameters.model_construct(loadName="hello"), # type: ignore[call-arg] + labware_def = LabwareDefinition2.model_construct( # type: ignore[call-arg] + parameters=Parameters2.model_construct(loadName="hello"), # type: ignore[call-arg] namespace="world", version=123, ) - standard_def = LabwareDefinition.model_construct( # type: ignore[call-arg] - parameters=Parameters.model_construct(loadName="goodbye"), # type: ignore[call-arg] + standard_def = LabwareDefinition2.model_construct( # type: ignore[call-arg] + parameters=Parameters2.model_construct(loadName="goodbye"), # type: ignore[call-arg] namespace="opentrons", version=456, ) @@ -546,6 +548,7 @@ def test_get_well_size_circular(well_plate_def: LabwareDefinition) -> None: definitions_by_uri={"some-plate-uri": well_plate_def}, ) expected_well_def = well_plate_def.wells["A2"] + assert expected_well_def.shape == "circular" # For type checking. expected_size = ( expected_well_def.diameter, expected_well_def.diameter, @@ -564,6 +567,7 @@ def test_get_well_size_rectangular(reservoir_def: LabwareDefinition) -> None: definitions_by_uri={"some-reservoir-uri": reservoir_def}, ) expected_well_def = reservoir_def.wells["A2"] + assert expected_well_def.shape == "rectangular" # For type checking. expected_size = ( expected_well_def.xDimension, expected_well_def.yDimension, @@ -614,12 +618,12 @@ def test_validate_liquid_allowed_raises_incompatible_labware() -> None: ), }, definitions_by_uri={ - "some-tiprack-uri": LabwareDefinition.model_construct( # type: ignore[call-arg] - parameters=Parameters.model_construct(isTiprack=True), # type: ignore[call-arg] + "some-tiprack-uri": LabwareDefinition2.model_construct( # type: ignore[call-arg] + parameters=Parameters2.model_construct(isTiprack=True), # type: ignore[call-arg] wells={}, ), - "some-adapter-uri": LabwareDefinition.model_construct( # type: ignore[call-arg] - parameters=Parameters.model_construct(isTiprack=False), # type: ignore[call-arg] + "some-adapter-uri": LabwareDefinition2.model_construct( # type: ignore[call-arg] + parameters=Parameters2.model_construct(isTiprack=False), # type: ignore[call-arg] allowedRoles=[LabwareRole.adapter], wells={}, ), @@ -661,8 +665,8 @@ def test_get_tip_length_gets_length_from_definition( def test_get_tip_drop_z_offset() -> None: """It should get a tip drop z offset by scaling the tip length.""" - tip_rack_def = LabwareDefinition.model_construct( # type: ignore[call-arg] - parameters=Parameters.model_construct( # type: ignore[call-arg] + tip_rack_def = LabwareDefinition2.model_construct( # type: ignore[call-arg] + parameters=Parameters2.model_construct( # type: ignore[call-arg] tipLength=100, ) ) @@ -756,7 +760,7 @@ def test_get_labware_overlap_offsets() -> None: """It should get the labware overlap offsets.""" subject = get_labware_view() result = subject.get_labware_overlap_offsets( - definition=LabwareDefinition.model_construct( # type: ignore[call-arg] + definition=LabwareDefinition2.model_construct( # type: ignore[call-arg] stackingOffsetWithLabware={"bottom-labware-name": Vector(x=1, y=2, z=3)} ), below_labware_name="bottom-labware-name", @@ -834,7 +838,7 @@ def test_get_module_overlap_offsets( deck_definition=spec_deck_definition, ) result = subject.get_module_overlap_offsets( - definition=LabwareDefinition.model_construct( # type: ignore[call-arg] + definition=LabwareDefinition2.model_construct( # type: ignore[call-arg] stackingOffsetWithModule=stacking_offset_with_module ), module_model=module_model, @@ -1281,7 +1285,7 @@ def test_get_edge_path_type( offsetId=None, ) - labware_def = LabwareDefinition.model_construct( # type: ignore[call-arg] + labware_def = LabwareDefinition2.model_construct( # type: ignore[call-arg] ordering=[["abc", "def"], ["ghi", "jkl"], ["mno", "pqr"]] ) @@ -1405,8 +1409,8 @@ def test_raise_if_labware_cannot_be_stacked_is_adapter() -> None: errors.LabwareCannotBeStackedError, match="defined as an adapter" ): subject.raise_if_labware_cannot_be_stacked( - top_labware_definition=LabwareDefinition.model_construct( # type: ignore[call-arg] - parameters=Parameters.model_construct(loadName="name"), # type: ignore[call-arg] + top_labware_definition=LabwareDefinition2.model_construct( # type: ignore[call-arg] + parameters=Parameters2.model_construct(loadName="name"), # type: ignore[call-arg] allowedRoles=[LabwareRole.adapter], ), bottom_labware_id="labware-id", @@ -1430,8 +1434,8 @@ def test_raise_if_labware_cannot_be_stacked_not_validated() -> None: errors.LabwareCannotBeStackedError, match="loaded onto labware test" ): subject.raise_if_labware_cannot_be_stacked( - top_labware_definition=LabwareDefinition.model_construct( # type: ignore[call-arg] - parameters=Parameters.model_construct(loadName="name"), # type: ignore[call-arg] + top_labware_definition=LabwareDefinition2.model_construct( # type: ignore[call-arg] + parameters=Parameters2.model_construct(loadName="name"), # type: ignore[call-arg] stackingOffsetWithLabware={}, ), bottom_labware_id="labware-id", @@ -1450,7 +1454,7 @@ def test_raise_if_labware_cannot_be_stacked_on_module_not_adapter() -> None: ) }, definitions_by_uri={ - "def-uri": LabwareDefinition.model_construct( # type: ignore[call-arg] + "def-uri": LabwareDefinition2.model_construct( # type: ignore[call-arg] allowedRoles=[LabwareRole.labware] ) }, @@ -1458,8 +1462,8 @@ def test_raise_if_labware_cannot_be_stacked_on_module_not_adapter() -> None: with pytest.raises(errors.LabwareCannotBeStackedError, match="module"): subject.raise_if_labware_cannot_be_stacked( - top_labware_definition=LabwareDefinition.model_construct( # type: ignore[call-arg] - parameters=Parameters.model_construct(loadName="name"), # type: ignore[call-arg] + top_labware_definition=LabwareDefinition2.model_construct( # type: ignore[call-arg] + parameters=Parameters2.model_construct(loadName="name"), # type: ignore[call-arg] stackingOffsetWithLabware={"test": Vector(x=0, y=0, z=0)}, ), bottom_labware_id="labware-id", @@ -1484,11 +1488,11 @@ def test_raise_if_labware_cannot_be_stacked_on_labware_on_adapter() -> None: ), }, definitions_by_uri={ - "def-uri-1": LabwareDefinition.model_construct( # type: ignore[call-arg] + "def-uri-1": LabwareDefinition2.model_construct( # type: ignore[call-arg] allowedRoles=[LabwareRole.labware], - parameters=Parameters.model_construct(loadName="test"), # type: ignore[call-arg] + parameters=Parameters2.model_construct(loadName="test"), # type: ignore[call-arg] ), - "def-uri-2": LabwareDefinition.model_construct( # type: ignore[call-arg] + "def-uri-2": LabwareDefinition2.model_construct( # type: ignore[call-arg] allowedRoles=[LabwareRole.adapter] ), }, @@ -1498,8 +1502,8 @@ def test_raise_if_labware_cannot_be_stacked_on_labware_on_adapter() -> None: errors.LabwareCannotBeStackedError, match="cannot be loaded to stack" ): subject.raise_if_labware_cannot_be_stacked( - top_labware_definition=LabwareDefinition.model_construct( # type: ignore[call-arg] - parameters=Parameters.model_construct(loadName="name"), # type: ignore[call-arg] + top_labware_definition=LabwareDefinition2.model_construct( # type: ignore[call-arg] + parameters=Parameters2.model_construct(loadName="name"), # type: ignore[call-arg] stackingOffsetWithLabware={"test": Vector(x=0, y=0, z=0)}, ), bottom_labware_id="labware-id", @@ -1559,9 +1563,9 @@ def test_labware_stacking_height_passes_or_raises( ), }, definitions_by_uri={ - "def-uri-1": LabwareDefinition.model_construct( # type: ignore[call-arg] + "def-uri-1": LabwareDefinition2.model_construct( # type: ignore[call-arg] allowedRoles=allowed_roles, - parameters=Parameters.model_construct( + parameters=Parameters2.model_construct( format="irregular", isTiprack=False, loadName="name", @@ -1574,8 +1578,8 @@ def test_labware_stacking_height_passes_or_raises( with exception: subject.raise_if_labware_cannot_be_stacked( - top_labware_definition=LabwareDefinition.model_construct( # type: ignore[call-arg] - parameters=Parameters.model_construct( + top_labware_definition=LabwareDefinition2.model_construct( # type: ignore[call-arg] + parameters=Parameters2.model_construct( format="irregular", isTiprack=False, loadName="name", @@ -1639,7 +1643,7 @@ def test_get_labware_gripper_offsets_default_no_slots( ) }, definitions_by_uri={ - "some-labware-uri": LabwareDefinition.model_construct( # type: ignore[call-arg] + "some-labware-uri": LabwareDefinition2.model_construct( # type: ignore[call-arg] gripperOffsets={ "default": GripperOffsets( pickUpOffset=Vector(x=1, y=2, z=3), @@ -1703,7 +1707,9 @@ def test_calculates_well_bounding_box( labware_to_check: str, well_bbox: Dimensions ) -> None: """It should be able to calculate well bounding boxes.""" - definition = LabwareDefinition.model_validate(load_definition(labware_to_check, 1)) + definition = labware_definition_type_adapter.validate_python( + load_definition(labware_to_check, 1) + ) subject = get_labware_view() assert subject.get_well_bbox(definition).x == pytest.approx(well_bbox.x) assert subject.get_well_bbox(definition).y == pytest.approx(well_bbox.y) diff --git a/api/tests/opentrons/protocol_engine/state/test_tip_state.py b/api/tests/opentrons/protocol_engine/state/test_tip_state.py index 7246a5f4cb2..e2691e22443 100644 --- a/api/tests/opentrons/protocol_engine/state/test_tip_state.py +++ b/api/tests/opentrons/protocol_engine/state/test_tip_state.py @@ -7,7 +7,8 @@ from opentrons_shared_data.labware.labware_definition import ( LabwareDefinition, - Parameters as LabwareParameters, + LabwareDefinition2, + Parameters2 as LabwareDefinition2Parameters, ) from opentrons_shared_data.pipette import pipette_definition from opentrons_shared_data.pipette.pipette_definition import ValidNozzleMaps @@ -32,7 +33,7 @@ get_default_nozzle_map, ) -_tip_rack_parameters = LabwareParameters.model_construct(isTiprack=True) # type: ignore[call-arg] +_tip_rack_parameters = LabwareDefinition2Parameters.model_construct(isTiprack=True) # type: ignore[call-arg] @pytest.fixture @@ -50,7 +51,7 @@ def subject() -> TipStore: @pytest.fixture def labware_definition() -> LabwareDefinition: """Get a labware definition value object.""" - return LabwareDefinition.model_construct( # type: ignore[call-arg] + return LabwareDefinition2.model_construct( # type: ignore[call-arg] ordering=[ ["A1", "B1", "C1", "D1", "E1", "F1", "G1", "H1"], ["A2", "B2", "C2", "D2", "E2", "F2", "G2", "H2"], @@ -95,7 +96,7 @@ def _dummy_command() -> commands.Command: @pytest.mark.parametrize( "labware_definition", - [LabwareDefinition.model_construct(ordering=[], parameters=_tip_rack_parameters)], # type: ignore[call-arg] + [LabwareDefinition2.model_construct(ordering=[], parameters=_tip_rack_parameters)], # type: ignore[call-arg] ) def test_get_next_tip_returns_none( load_labware_action: actions.SucceedCommandAction, @@ -926,9 +927,9 @@ def test_handle_pipette_config_action( @pytest.mark.parametrize( "labware_definition", [ - LabwareDefinition.model_construct( # type: ignore[call-arg] + LabwareDefinition2.model_construct( # type: ignore[call-arg] ordering=[["A1"]], - parameters=LabwareParameters.model_construct(isTiprack=False), # type: ignore[call-arg] + parameters=LabwareDefinition2Parameters.model_construct(isTiprack=False), # type: ignore[call-arg] ) ], ) diff --git a/api/tests/opentrons/protocol_runner/test_json_translator.py b/api/tests/opentrons/protocol_runner/test_json_translator.py index 3987feae334..25c9cd7ecb0 100644 --- a/api/tests/opentrons/protocol_runner/test_json_translator.py +++ b/api/tests/opentrons/protocol_runner/test_json_translator.py @@ -5,7 +5,8 @@ from opentrons_shared_data.labware.labware_definition import ( LabwareDefinition, - Parameters, + LabwareDefinition2, + Parameters2, Metadata, DisplayCategory, BrandData, @@ -13,10 +14,7 @@ Dimensions, Group, GroupMetadata, - WellDefinition, - CuboidalFrustum, - InnerWellGeometry, - SphericalSegment, + CircularWellDefinition2, ) from opentrons_shared_data.protocol.models import ( protocol_schema_v6, @@ -674,62 +672,33 @@ def subject() -> JsonTranslator: return JsonTranslator() -def _load_labware_definition_data() -> LabwareDefinition: - return LabwareDefinition( +def _load_labware_definition_data() -> LabwareDefinition2: + return LabwareDefinition2( version=1, namespace="example", schemaVersion=2, ordering=[["A1", "B1", "C1", "D1"], ["A2", "B2", "C2", "D2"]], groups=[Group(wells=["A1"], metadata=GroupMetadata())], wells={ - "A1": WellDefinition( + "A1": CircularWellDefinition2( depth=25, x=18.21, y=75.43, z=75, totalLiquidVolume=1100000, + diameter=1, shape="circular", ) }, dimensions=Dimensions(yDimension=85.5, zDimension=100, xDimension=127.75), cornerOffsetFromSlot=SD_Labware_Vector(x=0, y=0, z=0), - innerLabwareGeometry={ - "welldefinition1111": InnerWellGeometry( - sections=[ - CuboidalFrustum( - shape="cuboidal", - topXDimension=7.6, - topYDimension=8.5, - bottomXDimension=5.6, - bottomYDimension=6.5, - topHeight=45, - bottomHeight=20, - ), - CuboidalFrustum( - shape="cuboidal", - topXDimension=5.6, - topYDimension=6.5, - bottomXDimension=4.5, - bottomYDimension=4.0, - topHeight=20, - bottomHeight=10, - ), - SphericalSegment( - shape="spherical", - radiusOfCurvature=6, - topHeight=10, - bottomHeight=0.0, - ), - ], - ) - }, brand=BrandData(brand="foo"), metadata=Metadata( displayName="Foo 8 Well Plate 33uL", displayCategory=DisplayCategory("wellPlate"), displayVolumeUnits="µL", ), - parameters=Parameters( + parameters=Parameters2( loadName="foo_8_plate_33ul", isTiprack=False, isMagneticModuleCompatible=False, @@ -743,7 +712,7 @@ def _make_v6_json_protocol( pipettes: Dict[str, Pipette] = { "pipette-id-1": Pipette(name="p10_single"), }, - labware_definitions: Dict[str, LabwareDefinition] = { + labware_definitions: Dict[str, LabwareDefinition2] = { "example/plate/1": _load_labware_definition_data(), "example/trash/1": _load_labware_definition_data(), }, @@ -781,7 +750,7 @@ def _make_v6_json_protocol( def _make_v7_json_protocol( *, - labware_definitions: Dict[str, LabwareDefinition] = { + labware_definitions: Dict[str, LabwareDefinition2] = { "example/plate/1": _load_labware_definition_data(), "example/trash/1": _load_labware_definition_data(), }, diff --git a/api/tests/opentrons/protocol_runner/test_protocol_runner.py b/api/tests/opentrons/protocol_runner/test_protocol_runner.py index 2080ec69587..27d6dbd4529 100644 --- a/api/tests/opentrons/protocol_runner/test_protocol_runner.py +++ b/api/tests/opentrons/protocol_runner/test_protocol_runner.py @@ -8,7 +8,7 @@ from pathlib import Path from typing import List, cast, Union, Type -from opentrons_shared_data.labware.labware_definition import LabwareDefinition +from opentrons_shared_data.labware.labware_definition import LabwareDefinition2 from opentrons_shared_data.labware.types import ( LabwareDefinition as LabwareDefinitionTypedDict, ) @@ -361,7 +361,7 @@ async def test_run_json_runner_stop_requested_stops_enqueuing( json_translator: JsonTranslator, ) -> None: """It should run a protocol to completion.""" - labware_definition = LabwareDefinition.model_construct() # type: ignore[call-arg] + labware_definition = LabwareDefinition2.model_construct() # type: ignore[call-arg] json_protocol_source = ProtocolSource( directory=Path("/dev/null"), main_file=Path("/dev/null/abc.json"), @@ -467,7 +467,7 @@ async def test_load_json_runner( json_protocol: Union[ProtocolSchemaV6, ProtocolSchemaV7], ) -> None: """It should load a JSON protocol file.""" - labware_definition = LabwareDefinition.model_construct() # type: ignore[call-arg] + labware_definition = LabwareDefinition2.model_construct() # type: ignore[call-arg] json_protocol_source = ProtocolSource( directory=Path("/dev/null"), @@ -601,7 +601,7 @@ async def test_load_legacy_python( python_runner_subject: PythonAndLegacyRunner, ) -> None: """It should load a legacy context-based Python protocol.""" - labware_definition = LabwareDefinition.model_construct() # type: ignore[call-arg] + labware_definition = LabwareDefinition2.model_construct() # type: ignore[call-arg] legacy_protocol_source = ProtocolSource( directory=Path("/dev/null"), @@ -752,7 +752,7 @@ async def test_load_legacy_json( python_runner_subject: PythonAndLegacyRunner, ) -> None: """It should load a legacy context-based JSON protocol.""" - labware_definition = LabwareDefinition.model_construct() # type: ignore[call-arg] + labware_definition = LabwareDefinition2.model_construct() # type: ignore[call-arg] legacy_protocol_source = ProtocolSource( directory=Path("/dev/null"), diff --git a/robot-server/tests/maintenance_runs/router/test_labware_router.py b/robot-server/tests/maintenance_runs/router/test_labware_router.py index e1691c1b26b..c04383e232d 100644 --- a/robot-server/tests/maintenance_runs/router/test_labware_router.py +++ b/robot-server/tests/maintenance_runs/router/test_labware_router.py @@ -5,7 +5,10 @@ from decoy import Decoy from opentrons_shared_data.labware.types import LabwareDefinition as LabwareDefDict -from opentrons_shared_data.labware.labware_definition import LabwareDefinition +from opentrons_shared_data.labware.labware_definition import ( + LabwareDefinition, + labware_definition_type_adapter, +) from opentrons.types import DeckSlotName from opentrons.protocol_engine import EngineStatus, types as pe_types @@ -47,7 +50,7 @@ def run() -> MaintenanceRun: @pytest.fixture() def labware_definition(minimal_labware_def: LabwareDefDict) -> LabwareDefinition: """Create a labware definition fixture.""" - return LabwareDefinition.model_validate(minimal_labware_def) + return labware_definition_type_adapter.validate_python(minimal_labware_def) async def test_add_labware_offsets( diff --git a/robot-server/tests/runs/router/test_labware_router.py b/robot-server/tests/runs/router/test_labware_router.py index e8304784fbf..4836b587088 100644 --- a/robot-server/tests/runs/router/test_labware_router.py +++ b/robot-server/tests/runs/router/test_labware_router.py @@ -5,7 +5,10 @@ from decoy import Decoy from opentrons_shared_data.labware.types import LabwareDefinition as LabwareDefDict -from opentrons_shared_data.labware.labware_definition import LabwareDefinition +from opentrons_shared_data.labware.labware_definition import ( + LabwareDefinition, + labware_definition_type_adapter, +) from opentrons.types import DeckSlotName from opentrons.protocol_engine import EngineStatus, types as pe_types @@ -21,7 +24,7 @@ get_run_loaded_labware_definitions, ) from opentrons_shared_data.labware.labware_definition import ( - LabwareDefinition as SD_LabwareDefinition, + LabwareDefinition2 as SD_LabwareDefinition2, ) @@ -50,7 +53,7 @@ def run() -> Run: @pytest.fixture() def labware_definition(minimal_labware_def: LabwareDefDict) -> LabwareDefinition: """Create a labware definition fixture.""" - return LabwareDefinition.model_validate(minimal_labware_def) + return labware_definition_type_adapter.validate_python(minimal_labware_def) async def test_add_labware_offsets( @@ -195,8 +198,8 @@ async def test_get_run_labware_definition( mock_run_data_manager.get_run_loaded_labware_definitions(run_id="run-id") ).then_return( [ - SD_LabwareDefinition.model_construct(namespace="test_1"), # type: ignore[call-arg] - SD_LabwareDefinition.model_construct(namespace="test_2"), # type: ignore[call-arg] + SD_LabwareDefinition2.model_construct(namespace="test_1"), # type: ignore[call-arg] + SD_LabwareDefinition2.model_construct(namespace="test_2"), # type: ignore[call-arg] ] ) @@ -205,7 +208,7 @@ async def test_get_run_labware_definition( ) assert result.content.data == [ - SD_LabwareDefinition.model_construct(namespace="test_1"), # type: ignore[call-arg] - SD_LabwareDefinition.model_construct(namespace="test_2"), # type: ignore[call-arg] + SD_LabwareDefinition2.model_construct(namespace="test_1"), # type: ignore[call-arg] + SD_LabwareDefinition2.model_construct(namespace="test_2"), # type: ignore[call-arg] ] assert result.status_code == 200 diff --git a/robot-server/tests/runs/test_run_data_manager.py b/robot-server/tests/runs/test_run_data_manager.py index 8d139b4edff..3704170cc29 100644 --- a/robot-server/tests/runs/test_run_data_manager.py +++ b/robot-server/tests/runs/test_run_data_manager.py @@ -28,7 +28,7 @@ from opentrons.hardware_control.nozzle_manager import NozzleMap from opentrons_shared_data.errors.exceptions import InvalidStoredData -from opentrons_shared_data.labware.labware_definition import LabwareDefinition +from opentrons_shared_data.labware.labware_definition import LabwareDefinition2 from robot_server.error_recovery.settings.store import ErrorRecoverySettingStore from robot_server.protocols.protocol_models import ProtocolKind @@ -1264,16 +1264,16 @@ async def test_get_current_run_labware_definition( mock_run_orchestrator_store.get_loaded_labware_definitions() ).then_return( [ - LabwareDefinition.model_construct(namespace="test_1"), # type: ignore[call-arg] - LabwareDefinition.model_construct(namespace="test_2"), # type: ignore[call-arg] + LabwareDefinition2.model_construct(namespace="test_1"), # type: ignore[call-arg] + LabwareDefinition2.model_construct(namespace="test_2"), # type: ignore[call-arg] ] ) result = subject.get_run_loaded_labware_definitions(run_id="run-id") assert result == [ - LabwareDefinition.model_construct(namespace="test_1"), # type: ignore[call-arg] - LabwareDefinition.model_construct(namespace="test_2"), # type: ignore[call-arg] + LabwareDefinition2.model_construct(namespace="test_1"), # type: ignore[call-arg] + LabwareDefinition2.model_construct(namespace="test_2"), # type: ignore[call-arg] ] diff --git a/shared-data/python/opentrons_shared_data/labware/labware_definition.py b/shared-data/python/opentrons_shared_data/labware/labware_definition.py index e20c5baf27b..29daeda330b 100644 --- a/shared-data/python/opentrons_shared_data/labware/labware_definition.py +++ b/shared-data/python/opentrons_shared_data/labware/labware_definition.py @@ -7,15 +7,18 @@ from enum import Enum from math import sqrt, asin +from typing import Final from numpy import pi, trapz from functools import cached_property from pydantic import ( ConfigDict, BaseModel, + Discriminator, Field, StrictInt, StrictFloat, + TypeAdapter, ) from typing_extensions import Annotated, Literal @@ -95,7 +98,7 @@ class Metadata(BaseModel): tags: list[str] | None = None -class Parameters(BaseModel): +class Parameters2(BaseModel): format: Literal["96Standard", "384Standard", "trough", "irregular", "trash"] quirks: list[str] | None = None isTiprack: bool @@ -104,6 +107,9 @@ class Parameters(BaseModel): loadName: Annotated[str, Field(pattern=SAFE_STRING_REGEX)] isMagneticModuleCompatible: bool magneticModuleEngageHeight: _NonNegativeNumber | None = None + + +class Parameters3(Parameters2, BaseModel): isDeckSlotCompatible: bool | None = None @@ -113,21 +119,60 @@ class Dimensions(BaseModel): xDimension: _NonNegativeNumber -class WellDefinition(BaseModel): +class _WellCommon2(BaseModel): model_config = ConfigDict(extra="allow") depth: _NonNegativeNumber + totalLiquidVolume: _NonNegativeNumber x: _NonNegativeNumber y: _NonNegativeNumber z: _NonNegativeNumber + + +class CircularWellDefinition2(_WellCommon2, BaseModel): + shape: Literal["circular"] + diameter: _NonNegativeNumber + + +class RectangularWellDefinition2(_WellCommon2, BaseModel): + shape: Literal["rectangular"] + xDimension: _NonNegativeNumber + yDimension: _NonNegativeNumber + + +WellDefinition2 = Annotated[ + CircularWellDefinition2 | RectangularWellDefinition2, Field(discriminator="shape") +] + + +class _WellCommon3(BaseModel): + model_config = ConfigDict(extra="allow") + + depth: _NonNegativeNumber totalLiquidVolume: _NonNegativeNumber - xDimension: _NonNegativeNumber | None = None - yDimension: _NonNegativeNumber | None = None - diameter: _NonNegativeNumber | None = None - shape: Literal["rectangular", "circular"] + x: _NonNegativeNumber + y: _NonNegativeNumber + z: _NonNegativeNumber geometryDefinitionId: str | None = None +class CircularWellDefinition3(_WellCommon3, BaseModel): + shape: Literal["circular"] + diameter: _NonNegativeNumber + + +class RectangularWellDefinition3(_WellCommon3, BaseModel): + shape: Literal["rectangular"] + xDimension: _NonNegativeNumber + yDimension: _NonNegativeNumber + + +WellDefinition3 = Annotated[ + CircularWellDefinition3 | RectangularWellDefinition3, + Discriminator("shape"), +] + + class SphericalSegment(BaseModel): shape: Spherical radiusOfCurvature: _NonNegativeNumber @@ -430,7 +475,7 @@ class Group(BaseModel): | SquaredConeSegment | RoundedCuboidSegment | SphericalSegment, - Field(discriminator="shape"), + Discriminator("shape"), ] @@ -438,36 +483,60 @@ class InnerWellGeometry(BaseModel): sections: list[WellSegment] -class LabwareDefinition(BaseModel): - model_config = { - # `"extra": "allow"` gives us lossless preservation of the $otSharedSchema field - # (which should always be omitted in schema 2 and always be present in schema 3) - # across parse/serialize round trips. Pydantic doesn't seem to have a good way - # of doing that when the $otSharedSchema field is declared explicitly on this - # model. - # - # Splitting this model into separate ones for schemas 2 and 3 would also solve this. - "extra": "allow" - } - - # $otSharedSchema deliberately omitted for now. See `"extra": "allow"` in model_config. - schemaVersion: Literal[1, 2, 3] +class LabwareDefinition2(BaseModel): + # todo(mm, 2025-02-18): Is it correct to accept schemaVersion==1 here? + schemaVersion: Literal[1, 2] version: Annotated[int, Field(ge=1)] namespace: Annotated[str, Field(pattern=SAFE_STRING_REGEX)] metadata: Metadata brand: BrandData - parameters: Parameters - ordering: list[list[str]] + parameters: Parameters2 cornerOffsetFromSlot: Vector + ordering: list[list[str]] dimensions: Dimensions - wells: dict[str, WellDefinition] + wells: dict[str, WellDefinition2] groups: list[Group] - allowedRoles: list[LabwareRole] = Field(default_factory=list) stackingOffsetWithLabware: dict[str, Vector] = Field(default_factory=dict) stackingOffsetWithModule: dict[str, Vector] = Field(default_factory=dict) + allowedRoles: list[LabwareRole] = Field(default_factory=list) gripperOffsets: dict[str, GripperOffsets] = Field(default_factory=dict) + gripForce: float | None = None gripHeightFromLabwareBottom: float | None = None + stackLimit: int | None = None + + +class LabwareDefinition3(BaseModel): + otSharedSchema: Annotated[ + Literal["#/labware/schemas/3"], Field(alias="$otSharedSchema") + ] + schemaVersion: Literal[3] + version: Annotated[int, Field(ge=1)] + namespace: Annotated[str, Field(pattern=SAFE_STRING_REGEX)] + metadata: Metadata + brand: BrandData + parameters: Parameters3 + cornerOffsetFromSlot: Vector + ordering: list[list[str]] + dimensions: Dimensions + wells: dict[str, WellDefinition3] + groups: list[Group] + stackingOffsetWithLabware: dict[str, Vector] = Field(default_factory=dict) + stackingOffsetWithModule: dict[str, Vector] = Field(default_factory=dict) + allowedRoles: list[LabwareRole] = Field(default_factory=list) + gripperOffsets: dict[str, GripperOffsets] = Field(default_factory=dict) gripForce: float | None = None - innerLabwareGeometry: dict[str, InnerWellGeometry] | None = None + gripHeightFromLabwareBottom: float | None = None stackLimit: int | None = None + innerLabwareGeometry: dict[str, InnerWellGeometry] | None = None compatibleParentLabware: list[str] | None = None + + +LabwareDefinition = Annotated[ + LabwareDefinition2 | LabwareDefinition3, + Discriminator("schemaVersion"), +] + + +labware_definition_type_adapter: Final = TypeAdapter[LabwareDefinition]( + LabwareDefinition +) diff --git a/shared-data/python/opentrons_shared_data/labware/types.py b/shared-data/python/opentrons_shared_data/labware/types.py index 37c443e0582..39ec43a87b8 100644 --- a/shared-data/python/opentrons_shared_data/labware/types.py +++ b/shared-data/python/opentrons_shared_data/labware/types.py @@ -88,23 +88,21 @@ class LabwareDimensions(TypedDict): xDimension: float -class CircularWellDefinition2(TypedDict): - shape: CircularType +class _WellCommon2(TypedDict): depth: float totalLiquidVolume: float x: float y: float z: float + + +class CircularWellDefinition2(_WellCommon2, TypedDict): + shape: CircularType diameter: float -class RectangularWellDefinition2(TypedDict): +class RectangularWellDefinition2(_WellCommon2, TypedDict): shape: RectangularType - depth: float - totalLiquidVolume: float - x: float - y: float - z: float xDimension: float yDimension: float @@ -112,12 +110,24 @@ class RectangularWellDefinition2(TypedDict): WellDefinition2 = CircularWellDefinition2 | RectangularWellDefinition2 -class CircularWellDefinition3(CircularWellDefinition2, TypedDict): - geometryDefinitionId: NotRequired[str] +class _WellCommon3(TypedDict): + depth: float + totalLiquidVolume: float + x: float + y: float + z: float + geometryDefinitionId: NotRequired[str | None] -class RectangularWellDefinition3(RectangularWellDefinition2, TypedDict): - geometryDefinitionId: NotRequired[str | None] +class CircularWellDefinition3(_WellCommon3, TypedDict): + shape: CircularType + diameter: float + + +class RectangularWellDefinition3(_WellCommon3, TypedDict): + shape: RectangularType + xDimension: float + yDimension: float WellDefinition3 = CircularWellDefinition3 | RectangularWellDefinition3 @@ -181,12 +191,12 @@ class LabwareDefinition3(_OTSharedSchemaMixin, TypedDict): gripperOffsets: NotRequired[dict[str, GripperOffsets]] gripForce: NotRequired[float] gripHeightFromLabwareBottom: NotRequired[float] + stackLimit: NotRequired[int] # The innerLabwareGeometry dict values are not currently modeled in these # TypedDict-based bindings. The only code that cares about them # currentlyuses our Pydantic-based bindings instead. innerLabwareGeometry: NotRequired[dict[str, object] | None] compatibleParentLabware: NotRequired[list[str]] - stackLimit: NotRequired[int] LabwareDefinition = LabwareDefinition2 | LabwareDefinition3 diff --git a/shared-data/python/opentrons_shared_data/protocol/models/protocol_schema_v6.py b/shared-data/python/opentrons_shared_data/protocol/models/protocol_schema_v6.py index f8278d019f8..fb5588ba6bf 100644 --- a/shared-data/python/opentrons_shared_data/protocol/models/protocol_schema_v6.py +++ b/shared-data/python/opentrons_shared_data/protocol/models/protocol_schema_v6.py @@ -6,7 +6,7 @@ ) from typing import Any, List, Optional, Dict, Union from typing_extensions import Literal -from opentrons_shared_data.labware.labware_definition import LabwareDefinition +from opentrons_shared_data.labware.labware_definition import LabwareDefinition2 from .shared_models import ( Liquid, @@ -80,7 +80,7 @@ class ProtocolSchemaV6(BaseModel): labware: Dict[str, Labware] modules: Optional[Dict[str, Module]] = None liquids: Optional[Dict[str, Liquid]] = None - labwareDefinitions: Dict[str, LabwareDefinition] + labwareDefinitions: Dict[str, LabwareDefinition2] # commands must be after pipettes, labware, etc. for its @validator to work. commands: List[Command] commandAnnotations: Optional[List[CommandAnnotation]] = None diff --git a/shared-data/python/opentrons_shared_data/protocol/models/protocol_schema_v7.py b/shared-data/python/opentrons_shared_data/protocol/models/protocol_schema_v7.py index 97986ed385a..2c44b1d058e 100644 --- a/shared-data/python/opentrons_shared_data/protocol/models/protocol_schema_v7.py +++ b/shared-data/python/opentrons_shared_data/protocol/models/protocol_schema_v7.py @@ -2,7 +2,7 @@ from typing import Any, List, Optional, Dict, Union from typing_extensions import Literal -from opentrons_shared_data.labware.labware_definition import LabwareDefinition +from opentrons_shared_data.labware.labware_definition import LabwareDefinition2 from .shared_models import ( Liquid, @@ -85,7 +85,7 @@ class ProtocolSchemaV7(BaseModel): metadata: Metadata robot: Robot liquids: Optional[Dict[str, Liquid]] = None - labwareDefinitions: Dict[str, LabwareDefinition] + labwareDefinitions: Dict[str, LabwareDefinition2] commands: List[Command] commandAnnotations: Optional[List[CommandAnnotation]] = None designerApplication: Optional[DesignerApplication] = None diff --git a/shared-data/python/tests/labware/test_typechecks.py b/shared-data/python/tests/labware/test_typechecks.py index 5e1d87fa66b..57a3e8991c1 100644 --- a/shared-data/python/tests/labware/test_typechecks.py +++ b/shared-data/python/tests/labware/test_typechecks.py @@ -7,7 +7,9 @@ from opentrons_shared_data.labware import load_definition from opentrons_shared_data.labware.labware_definition import ( - LabwareDefinition as PydanticLabwareDefinition, + labware_definition_type_adapter as pydantic_labware_definition_type_adapter, + LabwareDefinition2 as PydanticLabwareDefinition2, + LabwareDefinition3 as PydanticLabwareDefinition3, ) from opentrons_shared_data.labware.types import ( LabwareDefinition as TypedDictLabwareDefinition, @@ -21,31 +23,57 @@ @pytest.mark.parametrize("loadname,version", get_ot_defs(schema=2)) def test_schema_2_types(loadname: str, version: int) -> None: + """Test parsing and validating into the types that represent schema 2.""" defdict = load_definition(loadname, version, schema=2) typeguard.check_type(defdict, TypedDictLabwareDefinition2) - typeguard.check_type(defdict, TypedDictLabwareDefinition) - PydanticLabwareDefinition.model_validate(defdict) + PydanticLabwareDefinition2.model_validate(defdict) @pytest.mark.parametrize("loadname,version", get_ot_defs(schema=3)) def test_schema_3_types(loadname: str, version: int) -> None: + """Test parsing and validating into the types that represent schema 3.""" defdict = load_definition(loadname, version, schema=3) typeguard.check_type(defdict, TypedDictLabwareDefinition3) + PydanticLabwareDefinition3.model_validate(defdict) + + +@pytest.mark.parametrize( + "loadname,version,schema_version", + [(loadname, version, 2) for loadname, version in get_ot_defs(schema=2)] + + [(loadname, version, 3) for loadname, version in get_ot_defs(schema=3)], +) +def test_all_schema_union_types( + loadname: str, version: int, schema_version: int +) -> None: + """Test parsing and validating into the types that represent a union of all schemas.""" + defdict = load_definition( + loadname=loadname, + version=version, + schema=schema_version, + ) + typeguard.check_type(defdict, TypedDictLabwareDefinition) - PydanticLabwareDefinition.model_validate(defdict) + + pydantic_result = pydantic_labware_definition_type_adapter.validate_python(defdict) + expected_result_type = ( + PydanticLabwareDefinition2 + if schema_version == 2 + else PydanticLabwareDefinition3 + ) + assert isinstance(pydantic_result, expected_result_type) def test_loadname_regex_applied() -> None: defdict = load_definition(*get_ot_defs(schema=2)[0]) defdict["parameters"]["loadName"] = "ALSJHDAKJLA" with pytest.raises(pydantic.ValidationError): - PydanticLabwareDefinition.model_validate(defdict) + PydanticLabwareDefinition2.model_validate(defdict) def test_namespace_regex_applied() -> None: defdict = load_definition(*get_ot_defs(schema=2)[0]) defdict["namespace"] = "ALSJHDAKJLA" with pytest.raises(pydantic.ValidationError): - PydanticLabwareDefinition.model_validate(defdict) + PydanticLabwareDefinition2.model_validate(defdict)