diff --git a/api/src/opentrons/protocol_api/core/engine/well.py b/api/src/opentrons/protocol_api/core/engine/well.py index bbf4bc95d3e..6743a8a39c5 100644 --- a/api/src/opentrons/protocol_api/core/engine/well.py +++ b/api/src/opentrons/protocol_api/core/engine/well.py @@ -6,7 +6,7 @@ from opentrons.protocol_engine import WellLocation, WellOrigin, WellOffset from opentrons.protocol_engine import commands as cmd from opentrons.protocol_engine.clients import SyncClient as EngineClient -from opentrons.protocols.api_support.util import APIVersionError +from opentrons.protocols.api_support.util import UnsupportedAPIError from opentrons.types import Point from . import point_calculations @@ -69,8 +69,8 @@ def has_tip(self) -> bool: def set_has_tip(self, value: bool) -> None: """Set the well as containing or not containing a tip.""" - raise APIVersionError( - "Manually setting the tip state of a well in a tip rack has been deprecated." + raise UnsupportedAPIError( + api_element="Manually setting the tip state of a well in a tip rack", ) def get_display_name(self) -> str: diff --git a/api/src/opentrons/protocol_api/core/legacy/legacy_instrument_core.py b/api/src/opentrons/protocol_api/core/legacy/legacy_instrument_core.py index 2e2aeba3ed9..ac1457c6c86 100644 --- a/api/src/opentrons/protocol_api/core/legacy/legacy_instrument_core.py +++ b/api/src/opentrons/protocol_api/core/legacy/legacy_instrument_core.py @@ -135,10 +135,10 @@ def dispense( """ if isinstance(location, (TrashBin, WasteChute)): raise APIVersionError( - "Dispense in Moveable Trash or Waste Chute are not supported in this API Version." + api_element="Dispense in Moveable Trash or Waste Chute" ) if push_out: - raise APIVersionError("push_out is not supported in this API version.") + raise APIVersionError(api_element="push_out") if not in_place: self.move_to(location=location) @@ -159,7 +159,7 @@ def blow_out( """ if isinstance(location, (TrashBin, WasteChute)): raise APIVersionError( - "Blow Out in Moveable Trash or Waste Chute are not supported in this API Version." + api_element="Blow Out in Moveable Trash or Waste Chute" ) if not in_place: @@ -249,9 +249,7 @@ def drop_tip( home_after: Whether to home the pipette after the tip is dropped. """ if alternate_drop_location: - raise APIVersionError( - "Tip drop randomization is not supported in this API version." - ) + raise APIVersionError(api_element="Tip drop randomization") labware_core = well_core.geometry.parent if location is None: @@ -302,9 +300,7 @@ def drop_tip_in_disposal_location( home_after: Optional[bool], alternate_tip_drop: bool = False, ) -> None: - raise APIVersionError( - "Dropping tips in a trash bin or waste chute is not supported in this API Version." - ) + raise APIVersionError(api_element="Dropping tips in a trash bin or waste chute") def home(self) -> None: """Home the mount""" @@ -342,9 +338,7 @@ def move_to( the computed safe travel height. """ if isinstance(location, (TrashBin, WasteChute)): - raise APIVersionError( - "Move To Trash Bin and Waste Chute are not supported in this API Version." - ) + raise APIVersionError(api_element="Move To Trash Bin and Waste Chute") self.flag_unsafe_move(location) # prevent direct movement bugs in PAPI version >= 2.10 diff --git a/api/src/opentrons/protocol_api/core/legacy/legacy_protocol_core.py b/api/src/opentrons/protocol_api/core/legacy/legacy_protocol_core.py index be35d699748..1c8181f1afb 100644 --- a/api/src/opentrons/protocol_api/core/legacy/legacy_protocol_core.py +++ b/api/src/opentrons/protocol_api/core/legacy/legacy_protocol_core.py @@ -139,7 +139,7 @@ def append_disposal_location( ) -> None: if isinstance(disposal_location, (TrashBin, WasteChute)): raise APIVersionError( - "Trash Bin and Waste Chute Disposal locations are not supported in this API Version." + api_element="Trash Bin and Waste Chute Disposal locations" ) self._disposal_locations.append(disposal_location) @@ -174,14 +174,17 @@ def load_labware( """Load a labware using its identifying parameters.""" if isinstance(location, OffDeckType): raise APIVersionError( - "Loading a labware off deck is only supported with apiLevel 2.15 and newer." + api_element="Loading a labware off deck", until_version="2.15" ) elif isinstance(location, LegacyLabwareCore): raise APIVersionError( - "Loading a labware onto another labware or adapter is only supported with api version 2.15 and above" + api_element="Loading a labware onto another labware or adapter", + until_version="2.15", ) elif isinstance(location, StagingSlotName): - raise APIVersionError("Using a staging deck slot requires apiLevel 2.16.") + raise APIVersionError( + api_element="Using a staging deck slot", until_version="2.16" + ) deck_slot = ( location if isinstance(location, DeckSlotName) else location.get_deck_slot() @@ -262,7 +265,7 @@ def load_adapter( version: Optional[int], ) -> LegacyLabwareCore: """Load an adapter using its identifying parameters""" - raise APIVersionError("Loading adapter is not supported in this API version.") + raise APIVersionError(api_element="Loading adapter") # TODO (spp, 2022-12-14): https://opentrons.atlassian.net/browse/RLAB-237 def move_labware( @@ -282,7 +285,7 @@ def move_labware( drop_offset: Optional[Tuple[float, float, float]], ) -> None: """Move labware to new location.""" - raise APIVersionError("Labware movement is not supported in this API version") + raise APIVersionError(api_element="Labware movement") def load_module( self, @@ -383,19 +386,15 @@ def load_instrument( return new_instr def load_trash_bin(self, slot_name: DeckSlotName, area_name: str) -> TrashBin: - raise APIVersionError( - "Loading deck configured trash bin is not supported in this API version." - ) + raise APIVersionError(api_element="Loading deck configured trash bin") def load_ot2_fixed_trash_bin(self) -> None: raise APIVersionError( - "Loading deck configured OT-2 fixed trash bin is not supported in this API version." + api_element="Loading deck configured OT-2 fixed trash bin" ) def load_waste_chute(self) -> WasteChute: - raise APIVersionError( - "Loading waste chute is not supported in this API version." - ) + raise APIVersionError(api_element="Loading waste chute") def get_loaded_instruments( self, diff --git a/api/src/opentrons/protocol_api/core/legacy/legacy_well_core.py b/api/src/opentrons/protocol_api/core/legacy/legacy_well_core.py index 81780f1006a..a88dd2eee80 100644 --- a/api/src/opentrons/protocol_api/core/legacy/legacy_well_core.py +++ b/api/src/opentrons/protocol_api/core/legacy/legacy_well_core.py @@ -112,7 +112,7 @@ def load_liquid( volume: float, ) -> None: """Load liquid into a well.""" - raise APIVersionError("Loading a liquid is not supported in this API version.") + raise APIVersionError(api_element="Loading a liquid") def from_center_cartesian(self, x: float, y: float, z: float) -> Point: """Gets point in deck coordinates based on percentage of the radius of each axis.""" diff --git a/api/src/opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py b/api/src/opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py index 27b964b4d61..574910568c8 100644 --- a/api/src/opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py +++ b/api/src/opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py @@ -135,7 +135,7 @@ def dispense( ) -> None: if isinstance(location, (TrashBin, WasteChute)): raise APIVersionError( - "Dispense in Moveable Trash or Waste Chute are not supported in this API Version." + api_element="Dispense in Moveable Trash or Waste Chute" ) if not in_place: self.move_to(location=location, well_core=well_core) @@ -150,7 +150,7 @@ def blow_out( ) -> None: if isinstance(location, (TrashBin, WasteChute)): raise APIVersionError( - "Blow Out in Moveable Trash or Waste Chute are not supported in this API Version." + api_element="Blow Out in Moveable Trash or Waste Chute" ) if not in_place: self.move_to(location=location, well_core=well_core) @@ -215,9 +215,7 @@ def drop_tip( alternate_drop_location: Optional[bool] = False, ) -> None: if alternate_drop_location: - raise APIVersionError( - "Tip drop alternation is not supported in this API version." - ) + raise APIVersionError(api_element="Tip drop alternation") labware_core = well_core.geometry.parent if location is None: @@ -270,9 +268,7 @@ def drop_tip_in_disposal_location( home_after: Optional[bool], alternate_tip_drop: bool = False, ) -> None: - raise APIVersionError( - "Dropping tips in a trash bin or waste chute is not supported in this API Version." - ) + raise APIVersionError(api_element="Dropping tips in a trash bin or waste chute") def home(self) -> None: self._protocol_interface.set_last_location(None) @@ -290,9 +286,7 @@ def move_to( ) -> None: """Simulation of only the motion planning portion of move_to.""" if isinstance(location, (TrashBin, WasteChute)): - raise APIVersionError( - "Move To Trash Bin and Waste Chute are not supported in this API Version." - ) + raise APIVersionError(api_element="Move To Trash Bin and Waste Chute") self.flag_unsafe_move(location) diff --git a/api/src/opentrons/protocol_api/deck.py b/api/src/opentrons/protocol_api/deck.py index 338b1bbecf6..b4ebe8ae766 100644 --- a/api/src/opentrons/protocol_api/deck.py +++ b/api/src/opentrons/protocol_api/deck.py @@ -107,8 +107,9 @@ def __delitem__(self, key: DeckLocation) -> None: # * PAPIv2.14 (Protocol Engine): No # * PAPIv2.15 (Protocol Engine): Yes raise APIVersionError( - f"Deleting deck elements is not supported with apiLevel {self._api_version}." - f" Try increasing your apiLevel to {APIVersion(2, 15)}." + api_element="Deleting deck elements", + until_version="2.15", + current_version=f"{self._api_version}", ) slot_name = _get_slot_name( diff --git a/api/src/opentrons/protocol_api/instrument_context.py b/api/src/opentrons/protocol_api/instrument_context.py index 4b77340bc7e..4bf466c84d5 100644 --- a/api/src/opentrons/protocol_api/instrument_context.py +++ b/api/src/opentrons/protocol_api/instrument_context.py @@ -27,6 +27,7 @@ clamp_value, requires_version, APIVersionError, + UnsupportedAPIError, ) from opentrons.hardware_control.nozzle_manager import NozzleConfigurationType @@ -369,7 +370,9 @@ def dispense( # noqa: C901 """ if self.api_version < APIVersion(2, 15) and push_out: raise APIVersionError( - "Unsupported parameter push_out. Change your API version to 2.15 or above to use this parameter." + api_element="Parameter push_out", + until_version="2.15", + current_version=f"{self.api_version}", ) _log.debug( "dispense {} from {} at {}".format( @@ -888,21 +891,24 @@ def pick_up_tip( # noqa: C901 """ if presses is not None and self._api_version >= _PRESSES_INCREMENT_REMOVED_IN: - raise APIVersionError( - f"presses is only available in API versions lower than {_PRESSES_INCREMENT_REMOVED_IN}," - f" but you are using API {self._api_version}." + raise UnsupportedAPIError( + api_element="presses", + since_version=f"{_PRESSES_INCREMENT_REMOVED_IN}", + current_version=f"{self._api_version}", ) if increment is not None and self._api_version >= _PRESSES_INCREMENT_REMOVED_IN: - raise APIVersionError( - f"increment is only available in API versions lower than {_PRESSES_INCREMENT_REMOVED_IN}," - f" but you are using API {self._api_version}." + raise UnsupportedAPIError( + api_element="increment", + since_version=f"{_PRESSES_INCREMENT_REMOVED_IN}", + current_version=f"{self._api_version}", ) if prep_after is not None and self._api_version < _PREP_AFTER_ADDED_IN: raise APIVersionError( - f"prep_after is only available in API {_PREP_AFTER_ADDED_IN} and newer," - f" but you are using API {self._api_version}." + api_element="prep_after", + until_version=f"{_PREP_AFTER_ADDED_IN}", + current_version=f"{self._api_version}", ) well: labware.Well @@ -1494,9 +1500,8 @@ def delay(self, *args: Any, **kwargs: Any) -> None: # would get a TypeError if they tried to call it like delay(minutes=10). # Without changing the ultimate behavior that such a call fails the # protocol, we can provide a more descriptive message as a courtesy. - raise APIVersionError( - "InstrumentContext.delay() is not supported in Python Protocol API v2." - " Use ProtocolContext.delay() instead." + raise UnsupportedAPIError( + message="InstrumentContext.delay() is not supported in Python Protocol API v2. Use ProtocolContext.delay() instead." ) else: # Former implementations of this method, when called without any args, @@ -1616,9 +1621,8 @@ def speed(self) -> "PlungerSpeeds": :py:attr:`.flow_rate` instead. """ if self._api_version >= ENGINE_CORE_API_VERSION: - raise APIVersionError( - "InstrumentContext.speed has been removed." - " Use InstrumentContext.flow_rate, instead." + raise UnsupportedAPIError( + message="InstrumentContext.speed has been removed. Use InstrumentContext.flow_rate, instead." ) # TODO(mc, 2023-02-13): this assert should be enough for mypy diff --git a/api/src/opentrons/protocol_api/labware.py b/api/src/opentrons/protocol_api/labware.py index be6cc442782..6e6932e0cf6 100644 --- a/api/src/opentrons/protocol_api/labware.py +++ b/api/src/opentrons/protocol_api/labware.py @@ -19,7 +19,11 @@ from opentrons.types import Location, Point from opentrons.protocols.api_support.types import APIVersion -from opentrons.protocols.api_support.util import requires_version, APIVersionError +from opentrons.protocols.api_support.util import ( + requires_version, + APIVersionError, + UnsupportedAPIError, +) from opentrons.hardware_control.nozzle_manager import NozzleMap # TODO(mc, 2022-09-02): re-exports provided for backwards compatibility @@ -126,7 +130,7 @@ def max_volume(self) -> float: def geometry(self) -> WellGeometry: if isinstance(self._core, LegacyWellCore): return self._core.geometry - raise APIVersionError("Well.geometry has been deprecated.") + raise UnsupportedAPIError(api_element="Well.geometry") @property @requires_version(2, 0) @@ -354,7 +358,11 @@ def __init__( @property def separate_calibration(self) -> bool: if self._api_version >= ENGINE_CORE_API_VERSION: - raise APIVersionError("Labware.separate_calibration has been removed") + raise UnsupportedAPIError( + api_element="Labware.separate_calibration", + since_version=f"{ENGINE_CORE_API_VERSION}", + current_version=f"{self._api_version}", + ) _log.warning( "Labware.separate_calibrations is a deprecated internal property." @@ -440,7 +448,11 @@ def name(self, new_name: str) -> None: Set the name of labware in `load_labware` instead. """ if self._api_version >= ENGINE_CORE_API_VERSION: - raise APIVersionError("Labware.name setter has been deprecated") + raise UnsupportedAPIError( + api_element="Labware.name setter", + since_version=f"{ENGINE_CORE_API_VERSION}", + current_version=f"{self._api_version}", + ) # TODO(mc, 2023-02-06): this assert should be enough for mypy # investigate if upgrading mypy allows the `cast` to be removed @@ -565,10 +577,11 @@ def set_calibration(self, delta: Point) -> None: .. deprecated:: 2.14 """ if self._api_version >= ENGINE_CORE_API_VERSION: - raise APIVersionError( - "Labware.set_calibration() is not supported when apiLevel is 2.14 or higher." - " Use a lower apiLevel" - " or use the Opentrons App's Labware Position Check." + raise UnsupportedAPIError( + api_element="Labware.set_calibration()", + since_version=f"{ENGINE_CORE_API_VERSION}", + current_version=f"{self._api_version}", + message=" Try using the Opentrons App's Labware Position Check.", ) self._core.set_calibration(delta) @@ -616,9 +629,10 @@ def set_offset(self, x: float, y: float, z: float) -> None: and self._api_version < SET_OFFSET_RESTORED_API_VERSION ): raise APIVersionError( - "Labware.set_offset() is not supported when apiLevel is 2.14, 2.15, 2.16, or 2.17." - " Use apilevel 2.13 or below, or 2.18 or above to set offset," - " or use the Opentrons App's Labware Position Check." + api_element="Labware.set_offset()", + until_version=f"{SET_OFFSET_RESTORED_API_VERSION}", + current_version=f"{self._api_version}", + message=" This feature not available in versions 2.14 thorugh 2.17. You can also use the Opentrons App's Labware Position Check.", ) else: self._core.set_calibration(Point(x=x, y=y, z=z)) @@ -889,7 +903,11 @@ def tip_length(self, length: float) -> None: and/or use the Opentrons App's tip length calibration feature. """ if self._api_version >= ENGINE_CORE_API_VERSION: - raise APIVersionError("Labware.tip_length setter has been deprecated") + raise UnsupportedAPIError( + api_element="Labware.tip_length setter", + since_version=f"{ENGINE_CORE_API_VERSION}", + current_version=f"{self._api_version}", + ) # TODO(mc, 2023-02-06): this assert should be enough for mypy # investigate if upgrading mypy allows the `cast` to be removed @@ -952,9 +970,11 @@ def use_tips(self, start_well: Well, num_channels: int = 1) -> None: Modification of tip tracking state outside :py:meth:`.reset` has been deprecated. """ if self._api_version >= ENGINE_CORE_API_VERSION: - raise APIVersionError( - "Labware.use_tips has been deprecated." - " To modify tip state, use Labware.reset" + raise UnsupportedAPIError( + api_element="Labware.use_tips", + since_version=f"{ENGINE_CORE_API_VERSION}", + current_version=f"{self._api_version}", + message=" To modify tip state, use Labware.reset.", ) assert num_channels > 0, "Bad call to use_tips: num_channels<=0" @@ -996,8 +1016,10 @@ def previous_tip(self, num_tips: int = 1) -> Optional[Well]: This method has been removed. """ if self._api_version >= ENGINE_CORE_API_VERSION: - raise APIVersionError( - "Labware.previous_tip is unsupported in this API version." + raise UnsupportedAPIError( + api_element="Labware.previous_tip", + since_version=f"{ENGINE_CORE_API_VERSION}", + current_version=f"{self._api_version}", ) # This logic is the inverse of :py:meth:`next_tip` @@ -1038,9 +1060,11 @@ def return_tips(self, start_well: Well, num_channels: int = 1) -> None: This method has been removed. Use :py:meth:`.reset` instead. """ if self._api_version >= ENGINE_CORE_API_VERSION: - raise APIVersionError( - "Labware.return_tips() is unsupported in this API version." - " Use Labware.reset() instead." + raise UnsupportedAPIError( + api_element="Labware.return_tips()", + since_version=f"{ENGINE_CORE_API_VERSION}", + current_version=f"{self._api_version}", + message=" Use Labware.reset() instead.", ) # This logic is the inverse of :py:meth:`use_tips` diff --git a/api/src/opentrons/protocol_api/module_contexts.py b/api/src/opentrons/protocol_api/module_contexts.py index e1a2fce844b..c8947049a96 100644 --- a/api/src/opentrons/protocol_api/module_contexts.py +++ b/api/src/opentrons/protocol_api/module_contexts.py @@ -11,7 +11,11 @@ from opentrons.legacy_commands import module_commands as cmds from opentrons.legacy_commands.publisher import CommandPublisher, publish from opentrons.protocols.api_support.types import APIVersion -from opentrons.protocols.api_support.util import APIVersionError, requires_version +from opentrons.protocols.api_support.util import ( + APIVersionError, + requires_version, + UnsupportedAPIError, +) from .core.common import ( ProtocolCore, @@ -95,15 +99,16 @@ def load_labware_object(self, labware: Labware) -> Labware: .. deprecated:: 2.14 Use :py:meth:`load_labware` or :py:meth:`load_labware_by_definition`. """ - deprecation_message = ( - "`ModuleContext.load_labware_object` is an internal, deprecated method." - " Use `ModuleContext.load_labware` or `load_labware_by_definition` instead." - ) - if not isinstance(self._core, LegacyModuleCore): - raise APIVersionError(deprecation_message) + raise UnsupportedAPIError( + api_element="`ModuleContext.load_labware_object`", + since_version="2.14", + message=" Use `ModuleContext.load_labware` or `load_labware_by_definition` instead.", + ) - _log.warning(deprecation_message) + _log.warning( + "`ModuleContext.load_labware_object` is an internal, deprecated method. Use `ModuleContext.load_labware` or `load_labware_by_definition` instead." + ) assert ( labware.parent == self._core.geometry @@ -143,7 +148,9 @@ def load_labware( if adapter is not None: if self._api_version < APIVersion(2, 15): raise APIVersionError( - "Loading a labware on an adapter requires apiLevel 2.15 or higher." + api_element="Loading a labware on an adapter", + until_version="2.15", + current_version=f"{self._api_version}", ) loaded_adapter = self.load_adapter( name=adapter, @@ -295,9 +302,10 @@ def geometry(self) -> LegacyModuleGeometry: if isinstance(self._core, LegacyModuleCore): return self._core.geometry - raise APIVersionError( - "`ModuleContext.geometry` has been deprecated;" - " use properties of the `ModuleContext` itself, instead." + raise UnsupportedAPIError( + api_element="`ModuleContext.geometry`", + since_version="2.14", + message=" Use properties of the `ModuleContext` itself.", ) def __repr__(self) -> str: @@ -426,7 +434,10 @@ def calibrate(self) -> None: ) self._core._sync_module_hardware.calibrate() # type: ignore[attr-defined] else: - raise APIVersionError("`MagneticModuleContext.calibrate` has been removed.") + raise UnsupportedAPIError( + api_element="`MagneticModuleContext.calibrate`", + since_version="2.14", + ) @publish(command=cmds.magdeck_engage) @requires_version(2, 0) @@ -467,10 +478,11 @@ def engage( """ if height is not None: if self._api_version >= _MAGNETIC_MODULE_HEIGHT_PARAM_REMOVED_IN: - raise APIVersionError( - f"The height parameter of MagneticModuleContext.engage() was removed" - f" in {_MAGNETIC_MODULE_HEIGHT_PARAM_REMOVED_IN}." - f" Use offset or height_from_base instead." + raise UnsupportedAPIError( + api_element="The height parameter of MagneticModuleContext.engage()", + since_version=f"{_MAGNETIC_MODULE_HEIGHT_PARAM_REMOVED_IN}", + current_version=f"{self._api_version}", + message=" Use offset or height_from_base.", ) self._core.engage(height_from_home=height) diff --git a/api/src/opentrons/protocol_api/protocol_context.py b/api/src/opentrons/protocol_api/protocol_context.py index 82d5f73fbd6..ad96e0c3156 100644 --- a/api/src/opentrons/protocol_api/protocol_context.py +++ b/api/src/opentrons/protocol_api/protocol_context.py @@ -39,6 +39,7 @@ requires_version, APIVersionError, RobotTypeError, + UnsupportedAPIError, ) from ._types import OffDeckType @@ -271,10 +272,11 @@ def max_speeds(self) -> AxisMaxSpeeds: if self._api_version >= ENGINE_CORE_API_VERSION: # TODO(mc, 2023-02-23): per-axis max speeds not yet supported on the engine # See https://opentrons.atlassian.net/browse/RCORE-373 - raise APIVersionError( - "ProtocolContext.max_speeds is not supported at apiLevel 2.14 or higher." - " Use a lower apiLevel or set speeds using InstrumentContext.default_speed" - " or the per-method 'speed' argument." + raise UnsupportedAPIError( + api_element="ProtocolContext.max_speeds", + since_version=f"{ENGINE_CORE_API_VERSION}", + current_version=f"{self._api_version}", + message=" Set speeds using InstrumentContext.default_speed or the per-method 'speed' argument.", ) return self._core.get_max_speeds() @@ -421,7 +423,9 @@ def load_labware( """ if isinstance(location, OffDeckType) and self._api_version < APIVersion(2, 15): raise APIVersionError( - "Loading a labware off-deck requires apiLevel 2.15 or higher." + api_element="Loading a labware off-deck", + until_version="2.15", + current_version=f"{self._api_version}", ) load_name = validation.ensure_lowercase_name(load_name) @@ -429,7 +433,9 @@ def load_labware( if adapter is not None: if self._api_version < APIVersion(2, 15): raise APIVersionError( - "Loading a labware on an adapter requires apiLevel 2.15 or higher." + api_element="Loading a labware on an adapter", + until_version="2.15", + current_version=f"{self._api_version}", ) loaded_adapter = self.load_adapter( load_name=adapter, @@ -798,12 +804,15 @@ def load_module( if configuration: if self._api_version < APIVersion(2, 4): raise APIVersionError( - f"You have specified API {self._api_version}, but you are" - "using Thermocycler parameters only available in 2.4" + api_element="Thermocycler parameters", + until_version="2.4", + current_version=f"{self._api_version}", ) if self._api_version >= ENGINE_CORE_API_VERSION: - raise APIVersionError( - "The configuration parameter of load_module has been removed." + raise UnsupportedAPIError( + api_element="The configuration parameter of load_module", + since_version=f"{ENGINE_CORE_API_VERSION}", + current_version=f"{self._api_version}", ) requested_model = validation.ensure_module_model(module_name) @@ -811,7 +820,9 @@ def load_module( requested_model, MagneticBlockModel ) and self._api_version < APIVersion(2, 15): raise APIVersionError( - f"Module of type {module_name} is only available in versions 2.15 and above." + api_element=f"Module of type {module_name}", + until_version="2.15", + current_version=f"{self._api_version}", ) deck_slot = ( @@ -936,7 +947,9 @@ def load_instrument( and liquid_presence_detection is not None ): raise APIVersionError( - "Liquid Presence Detection is only supported in API Version 2.20 and above." + api_element="Liquid Presence Detection", + until_version="2.20", + current_version=f"{self._api_version}", ) if ( self._core.robot_type != "OT-3 Standard" @@ -961,7 +974,7 @@ def load_instrument( trash: Optional[Union[Labware, TrashBin]] try: trash = self.fixed_trash - except (NoTrashDefinedError, APIVersionError): + except (NoTrashDefinedError, UnsupportedAPIError): trash = None instrument = InstrumentContext( @@ -1026,9 +1039,11 @@ def resume(self) -> None: after a period of time, use :py:meth:`delay`. """ if self._api_version >= ENGINE_CORE_API_VERSION: - raise APIVersionError( - "A Python Protocol cannot safely resume itself after a pause." - " To wait automatically for a period of time, use ProtocolContext.delay()." + raise UnsupportedAPIError( + api_element="A Python Protocol safely resuming itself after a pause", + since_version=f"{ENGINE_CORE_API_VERSION}", + current_version=f"{self._api_version}", + message=" To wait automatically for a period of time, use ProtocolContext.delay().", ) # TODO(mc, 2023-02-13): this assert should be enough for mypy @@ -1145,8 +1160,11 @@ def fixed_trash(self) -> Union[Labware, TrashBin]: """ if self._api_version >= APIVersion(2, 16): if self._core.robot_type == "OT-3 Standard": - raise APIVersionError( - "Fixed Trash is not supported on Flex protocols in API Version 2.16 and above." + raise UnsupportedAPIError( + api_element="Fixed Trash", + since_version="2.16", + current_version=f"{self._api_version}", + message=" Fixed trash is no longer supported on Flex protocols.", ) disposal_locations = self._core.get_disposal_locations() if len(disposal_locations) == 0: diff --git a/api/src/opentrons/protocol_api/validation.py b/api/src/opentrons/protocol_api/validation.py index e1c1902023f..62e5ecc3dc1 100644 --- a/api/src/opentrons/protocol_api/validation.py +++ b/api/src/opentrons/protocol_api/validation.py @@ -189,7 +189,9 @@ def ensure_and_convert_deck_slot( if str(deck_slot).upper() in {"A4", "B4", "C4", "D4"}: if api_version < APIVersion(2, 16): raise APIVersionError( - f"Using a staging deck slot requires apiLevel {_STAGING_DECK_SLOT_VERSION_GATE}." + api_element="Using a staging deck slot", + until_version=f"{_STAGING_DECK_SLOT_VERSION_GATE}", + current_version=f"{api_version}", ) # Don't need a try/except since we're already pre-validating this parsed_staging_slot = StagingSlotName.from_primitive(str(deck_slot)) @@ -203,9 +205,10 @@ def ensure_and_convert_deck_slot( if not is_ot2_style and api_version < _COORDINATE_DECK_LABEL_VERSION_GATE: alternative = parsed_slot.to_ot2_equivalent().id raise APIVersionError( - f'Specifying a deck slot like "{deck_slot}" requires apiLevel' - f" {_COORDINATE_DECK_LABEL_VERSION_GATE}." - f' Increase your protocol\'s apiLevel, or use slot "{alternative}" instead.' + api_element=f"Specifying a deck slot like '{deck_slot}'", + until_version=f"{_COORDINATE_DECK_LABEL_VERSION_GATE}", + current_version=f"{api_version}", + message=f" Increase your protocol's apiLevel, or use slot '{alternative}' instead.", ) return parsed_slot.to_equivalent_for_robot_type(robot_type) diff --git a/api/src/opentrons/protocols/api_support/util.py b/api/src/opentrons/protocols/api_support/util.py index d5c988b153c..e1eb1195b12 100644 --- a/api/src/opentrons/protocols/api_support/util.py +++ b/api/src/opentrons/protocols/api_support/util.py @@ -23,6 +23,8 @@ from opentrons.hardware_control.util import ot2_axis_to_string from opentrons_shared_data.robot.dev_types import RobotType from opentrons_shared_data.errors.exceptions import ( + APIRemoved, + IncorrectAPIVersion, UnsupportedHardwareCommand, ) @@ -46,13 +48,13 @@ class RobotTypeError(UnsupportedHardwareCommand): pass -class APIVersionError(Exception): +class APIVersionError(IncorrectAPIVersion): """Error raised when a protocol attempts to access behavior not implemented in the API in use.""" pass -class UnsupportedAPIError(Exception): +class UnsupportedAPIError(APIRemoved): """Error raised when a protocol attempts to use unsupported API.""" pass @@ -384,10 +386,9 @@ def _check_version_wrapper(*args: Any, **kwargs: Any) -> Any: name = getattr(decorated_obj, "__qualname__", str(decorated_obj)) raise APIVersionError( - f"{name} was added in {added_in}, but your " - f"protocol requested version {current_version}. You " - f"must increase your API version to {added_in} to " - "use this functionality." + api_element=name, + until_version=added_in, + current_version=current_version, ) return decorated_obj(*args, **kwargs) 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 5d2d5f4914b..31b562f7e81 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 @@ -11,7 +11,7 @@ from opentrons.protocol_engine import commands as cmd from opentrons.protocol_engine.clients import SyncClient as EngineClient from opentrons.protocols.api_support.types import APIVersion -from opentrons.protocols.api_support.util import APIVersionError +from opentrons.protocols.api_support.util import UnsupportedAPIError from opentrons.types import Point from opentrons.protocol_api._liquid import Liquid @@ -164,7 +164,7 @@ def test_has_tip( def test_set_has_tip(subject: WellCore) -> None: """Trying to set the has tip state should raise an error.""" - with pytest.raises(APIVersionError): + with pytest.raises(UnsupportedAPIError): subject.set_has_tip(True) diff --git a/api/tests/opentrons/protocol_api/test_instrument_context.py b/api/tests/opentrons/protocol_api/test_instrument_context.py index ab5d099460e..6fd65bd41fc 100644 --- a/api/tests/opentrons/protocol_api/test_instrument_context.py +++ b/api/tests/opentrons/protocol_api/test_instrument_context.py @@ -13,6 +13,7 @@ from opentrons.protocols.api_support.types import APIVersion from opentrons.protocols.api_support.util import ( APIVersionError, + UnsupportedAPIError, FlowRates, PlungerSpeeds, ) @@ -265,7 +266,7 @@ def test_pick_up_from_well_deprecated_args( """It should pick up a specific tip.""" mock_well = decoy.mock(cls=Well) - with pytest.raises(APIVersionError): + with pytest.raises(UnsupportedAPIError): subject.pick_up_tip(mock_well, presses=1, increment=2.0, prep_after=False) @@ -1091,7 +1092,7 @@ def test_plunger_speed( @pytest.mark.parametrize("api_version", [APIVersion(2, 14)]) def test_plunger_speed_removed(subject: InstrumentContext) -> None: """It should raise an error on PAPI >= v2.14.""" - with pytest.raises(APIVersionError): + with pytest.raises(UnsupportedAPIError): subject.speed diff --git a/api/tests/opentrons/protocol_api/test_labware.py b/api/tests/opentrons/protocol_api/test_labware.py index b9b008e77a1..bfbbb7b33a7 100644 --- a/api/tests/opentrons/protocol_api/test_labware.py +++ b/api/tests/opentrons/protocol_api/test_labware.py @@ -8,7 +8,7 @@ from opentrons_shared_data.labware.dev_types import LabwareDefinition as LabwareDefDict from opentrons.protocols.api_support.types import APIVersion -from opentrons.protocols.api_support.util import APIVersionError +from opentrons.protocols.api_support.util import APIVersionError, UnsupportedAPIError from opentrons.protocol_api import MAX_SUPPORTED_VERSION, Labware, Well from opentrons.protocol_api.core import well_grid from opentrons.protocol_api.core.common import ( @@ -362,5 +362,5 @@ def test_separate_calibration_raises_on_high_api_version( mock_labware_core: LabwareCore, ) -> None: """It should raise an error, on high API versions.""" - with pytest.raises(APIVersionError): + with pytest.raises(UnsupportedAPIError): subject.separate_calibration diff --git a/api/tests/opentrons/protocol_api/test_magnetic_module_context.py b/api/tests/opentrons/protocol_api/test_magnetic_module_context.py index 6435d5d8787..db980dced99 100644 --- a/api/tests/opentrons/protocol_api/test_magnetic_module_context.py +++ b/api/tests/opentrons/protocol_api/test_magnetic_module_context.py @@ -5,7 +5,7 @@ from opentrons.legacy_broker import LegacyBroker from opentrons.hardware_control.modules import MagneticStatus from opentrons.protocols.api_support.types import APIVersion -from opentrons.protocols.api_support.util import APIVersionError +from opentrons.protocols.api_support.util import UnsupportedAPIError from opentrons.protocol_api import MAX_SUPPORTED_VERSION, MagneticModuleContext from opentrons.protocol_api.core.common import ProtocolCore, MagneticModuleCore from opentrons.protocol_api.core.core_map import LoadedCoreMap @@ -117,9 +117,9 @@ def test_engage_height_from_home_raises_on_high_version( subject: MagneticModuleContext, ) -> None: """It should error if given a raw motor height and the apiLevel is high.""" - with pytest.raises(APIVersionError): + with pytest.raises(UnsupportedAPIError): subject.engage(height=42.0) - with pytest.raises(APIVersionError): + with pytest.raises(UnsupportedAPIError): subject.engage(42.0) @@ -130,7 +130,7 @@ def test_calibrate_raises_on_high_version( subject: MagneticModuleContext, ) -> None: """It should raise a deprecation error.""" - with pytest.raises(APIVersionError): + with pytest.raises(UnsupportedAPIError): subject.calibrate() diff --git a/api/tests/opentrons/protocol_api/test_protocol_context.py b/api/tests/opentrons/protocol_api/test_protocol_context.py index 7f00bfb9ee8..a72ed7f8856 100644 --- a/api/tests/opentrons/protocol_api/test_protocol_context.py +++ b/api/tests/opentrons/protocol_api/test_protocol_context.py @@ -14,7 +14,11 @@ from opentrons.hardware_control.modules.types import ModuleType, TemperatureModuleModel from opentrons.protocols.api_support import instrument as mock_instrument_support from opentrons.protocols.api_support.types import APIVersion -from opentrons.protocols.api_support.util import APIVersionError, RobotTypeError +from opentrons.protocols.api_support.util import ( + APIVersionError, + RobotTypeError, + UnsupportedAPIError, +) from opentrons.protocol_api import ( MAX_SUPPORTED_VERSION, ProtocolContext, @@ -1070,7 +1074,7 @@ def test_load_module_default_location( @pytest.mark.parametrize("api_version", [APIVersion(2, 14)]) def test_load_module_with_configuration(subject: ProtocolContext) -> None: """It should raise an APIVersionError if the deprecated `configuration` argument is used.""" - with pytest.raises(APIVersionError, match="removed"): + with pytest.raises(UnsupportedAPIError): subject.load_module( module_name="spline reticulator", location=42, @@ -1081,7 +1085,7 @@ def test_load_module_with_configuration(subject: ProtocolContext) -> None: @pytest.mark.parametrize("api_version", [APIVersion(2, 14)]) def test_load_module_with_mag_block_raises(subject: ProtocolContext) -> None: """It should raise an APIVersionError if loading a magnetic block.""" - with pytest.raises(APIVersionError): + with pytest.raises(UnsupportedAPIError): subject.load_module( module_name="magneticBlockV1", location=42, diff --git a/api/tests/opentrons/protocol_api/test_validation.py b/api/tests/opentrons/protocol_api/test_validation.py index 667349f0f8d..b06e28e0785 100644 --- a/api/tests/opentrons/protocol_api/test_validation.py +++ b/api/tests/opentrons/protocol_api/test_validation.py @@ -4,6 +4,7 @@ from decoy import Decoy import pytest +import re from opentrons_shared_data.labware.labware_definition import ( LabwareRole, @@ -188,7 +189,9 @@ def test_ensure_and_convert_deck_slot( "A1", APIVersion(2, 0), APIVersionError, - '"A1" requires apiLevel 2.15. Increase your protocol\'s apiLevel, or use slot "10" instead.', + re.escape( + "Error 4011 INCORRECT_API_VERSION (APIVersionError): Specifying a deck slot like 'A1' is not available until API version 2.15. You are currently using API version 2.0. Increase your protocol's apiLevel, or use slot '10' instead." + ), ), ("A4", APIVersion(2, 15), APIVersionError, "Using a staging deck slot"), ], diff --git a/api/tests/opentrons/protocol_api_integration/test_trashes.py b/api/tests/opentrons/protocol_api_integration/test_trashes.py index 1c8250fe44e..18dfa62170d 100644 --- a/api/tests/opentrons/protocol_api_integration/test_trashes.py +++ b/api/tests/opentrons/protocol_api_integration/test_trashes.py @@ -3,10 +3,12 @@ from opentrons import protocol_api, simulate from opentrons.protocols.api_support.types import APIVersion +from opentrons.protocols.api_support.util import UnsupportedAPIError import contextlib from typing import ContextManager, Optional, Type from typing_extensions import Literal +import re import pytest @@ -55,8 +57,10 @@ def test_fixed_trash_presence( if expected_trash_class is None: with pytest.raises( - Exception, - match="Fixed Trash is not supported on Flex protocols in API Version 2.16 and above.", + UnsupportedAPIError, + match=re.escape( + "Error 4002 API_REMOVED (UnsupportedAPIError): Fixed Trash is not available after API version 2.16. You are currently using API version 2.16. Fixed trash is no longer supported on Flex protocols." + ), ): protocol.fixed_trash with pytest.raises(Exception, match="No trash container has been defined"): @@ -75,8 +79,10 @@ def test_trash_search() -> None: # By default, there should be no trash. with pytest.raises( - Exception, - match="Fixed Trash is not supported on Flex protocols in API Version 2.16 and above.", + UnsupportedAPIError, + match=re.escape( + "Error 4002 API_REMOVED (UnsupportedAPIError): Fixed Trash is not available after API version 2.16. You are currently using API version 2.16. Fixed trash is no longer supported on Flex protocols." + ), ): protocol.fixed_trash with pytest.raises(Exception, match="No trash container has been defined"): @@ -87,8 +93,10 @@ def test_trash_search() -> None: # After loading some trashes, there should still be no protocol.fixed_trash... with pytest.raises( - Exception, - match="Fixed Trash is not supported on Flex protocols in API Version 2.16 and above.", + UnsupportedAPIError, + match=re.escape( + "Error 4002 API_REMOVED (UnsupportedAPIError): Fixed Trash is not available after API version 2.16. You are currently using API version 2.16. Fixed trash is no longer supported on Flex protocols." + ), ): protocol.fixed_trash # ...but instrument.trash_container should automatically update to point to diff --git a/shared-data/errors/definitions/1/errors.json b/shared-data/errors/definitions/1/errors.json index 1aca5ccf495..cff28ddd064 100644 --- a/shared-data/errors/definitions/1/errors.json +++ b/shared-data/errors/definitions/1/errors.json @@ -249,6 +249,10 @@ "4010": { "detail": "Runtime Parameter Value Required", "category": "generalError" + }, + "4011": { + "detail": "Incorrect API Version", + "category": "generalError" } } } diff --git a/shared-data/python/opentrons_shared_data/errors/codes.py b/shared-data/python/opentrons_shared_data/errors/codes.py index bfd0ecbdbbd..9b767d09733 100644 --- a/shared-data/python/opentrons_shared_data/errors/codes.py +++ b/shared-data/python/opentrons_shared_data/errors/codes.py @@ -92,6 +92,7 @@ class ErrorCodes(Enum): INVALID_STORED_DATA = _code_from_dict_entry("4008") MISSING_CONFIGURATION_DATA = _code_from_dict_entry("4009") RUNTIME_PARAMETER_VALUE_REQUIRED = _code_from_dict_entry("4010") + INCORRECT_API_VERSION = _code_from_dict_entry("4011") @classmethod @lru_cache(25) diff --git a/shared-data/python/opentrons_shared_data/errors/exceptions.py b/shared-data/python/opentrons_shared_data/errors/exceptions.py index 43d94e11a0b..888dc7f6763 100644 --- a/shared-data/python/opentrons_shared_data/errors/exceptions.py +++ b/shared-data/python/opentrons_shared_data/errors/exceptions.py @@ -902,8 +902,9 @@ class APIRemoved(GeneralError): def __init__( self, - api_element: str, - since_version: str, + api_element: Optional[str] = None, + since_version: Optional[str] = None, + current_version: Optional[str] = None, message: Optional[str] = None, detail: Optional[Dict[str, str]] = None, wrapping: Optional[Sequence[EnumeratedError]] = None, @@ -912,15 +913,67 @@ def __init__( checked_detail: Dict[str, Any] = detail or {} checked_detail["identifier"] = api_element checked_detail["since_version"] = since_version + checked_detail["current_version"] = current_version + checked_message = "" + if api_element and since_version and current_version: + checked_message = f"{api_element} is not available after API version {since_version}. You are currently using API version {current_version}." + elif api_element and since_version: + checked_message = ( + f"{api_element} is not available after API version {since_version}." + ) + elif api_element: + checked_message = ( + f"{api_element} is no longer available in the API version in use." + ) + if message: + checked_message = checked_message + message checked_message = ( - message - or f"{api_element} is no longer available since version {since_version}." + checked_message + or "This feature is no longer available in the API version in use." ) super().__init__( ErrorCodes.API_REMOVED, checked_message, checked_detail, wrapping ) +class IncorrectAPIVersion(GeneralError): + """An error indicating that a command was issued that is not supported by the API version in use.""" + + def __init__( + self, + api_element: Optional[str] = None, + until_version: Optional[str] = None, + current_version: Optional[str] = None, + message: Optional[str] = None, + detail: Optional[Dict[str, str]] = None, + wrapping: Optional[Sequence[EnumeratedError]] = None, + ) -> None: + """Build an IncorrectAPIVersion error.""" + checked_detail: Dict[str, Any] = detail or {} + checked_detail["identifier"] = api_element + checked_detail["until_version"] = until_version + checked_detail["current_version"] = current_version + if api_element and until_version and current_version: + checked_message = f"{api_element} is not available until API version {until_version}. You are currently using API version {current_version}." + elif api_element and until_version: + checked_message = ( + f"{api_element} is not available until API version {until_version}." + ) + elif api_element: + checked_message = ( + f"{api_element} is not yet available in the API version in use." + ) + if message: + checked_message = checked_message + message + checked_message = ( + checked_message + or "This feature is not yet available in the API version in use." + ) + super().__init__( + ErrorCodes.INCORRECT_API_VERSION, checked_message, checked_detail, wrapping + ) + + class CommandPreconditionViolated(GeneralError): """An error indicating that a command was issued in a robot state incompatible with it."""