Skip to content

Commit d20f59c

Browse files
feat(api): Alias del deck[...] to moveLabware(..., OFF_DECK) (#13239)
1 parent 880a5de commit d20f59c

File tree

9 files changed

+157
-10
lines changed

9 files changed

+157
-10
lines changed

api/src/opentrons/protocol_api/core/engine/protocol.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -256,17 +256,20 @@ def move_labware(
256256
DeckSlotName, LabwareCore, ModuleCore, NonConnectedModuleCore, OffDeckType
257257
],
258258
use_gripper: bool,
259+
pause_for_manual_move: bool,
259260
pick_up_offset: Optional[Tuple[float, float, float]],
260261
drop_offset: Optional[Tuple[float, float, float]],
261262
) -> None:
262263
"""Move the given labware to a new location."""
263264
to_location = self._convert_labware_location(location=new_location)
264265

265-
strategy = (
266-
LabwareMovementStrategy.USING_GRIPPER
267-
if use_gripper
268-
else LabwareMovementStrategy.MANUAL_MOVE_WITH_PAUSE
269-
)
266+
if use_gripper:
267+
strategy = LabwareMovementStrategy.USING_GRIPPER
268+
elif pause_for_manual_move:
269+
strategy = LabwareMovementStrategy.MANUAL_MOVE_WITH_PAUSE
270+
else:
271+
strategy = LabwareMovementStrategy.MANUAL_MOVE_WITHOUT_PAUSE
272+
270273
_pick_up_offset = (
271274
LabwareOffsetVector(
272275
x=pick_up_offset[0], y=pick_up_offset[1], z=pick_up_offset[2]

api/src/opentrons/protocol_api/core/legacy/legacy_protocol_core.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,7 @@ def move_labware(
254254
OffDeckType,
255255
],
256256
use_gripper: bool,
257+
pause_for_manual_move: bool,
257258
pick_up_offset: Optional[Tuple[float, float, float]],
258259
drop_offset: Optional[Tuple[float, float, float]],
259260
) -> None:

api/src/opentrons/protocol_api/core/module.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ def get_deck_slot(self) -> DeckSlotName:
3636
def get_deck_slot_id(self) -> str:
3737
"""Get the module's deck slot in a robot accurate format."""
3838

39+
@abstractmethod
3940
def get_display_name(self) -> str:
4041
"""Get the module's display name."""
4142

api/src/opentrons/protocol_api/core/protocol.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ def move_labware(
8686
labware_core: LabwareCoreType,
8787
new_location: Union[DeckSlotName, LabwareCoreType, ModuleCoreType, OffDeckType],
8888
use_gripper: bool,
89+
pause_for_manual_move: bool,
8990
pick_up_offset: Optional[Tuple[float, float, float]],
9091
drop_offset: Optional[Tuple[float, float, float]],
9192
) -> None:

api/src/opentrons/protocol_api/deck.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,17 @@
66

77
from opentrons.motion_planning import adjacent_slots_getters
88
from opentrons.protocols.api_support.types import APIVersion
9+
from opentrons.protocols.api_support.util import APIVersionError
910
from opentrons.types import DeckLocation, DeckSlotName, Location, Point
1011
from opentrons_shared_data.robot.dev_types import RobotType
1112

13+
1214
from .core.common import ProtocolCore
1315
from .core.core_map import LoadedCoreMap
16+
from .core.module import AbstractModuleCore
1417
from .labware import Labware
1518
from .module_contexts import ModuleContext
19+
from ._types import OFF_DECK
1620
from . import validation
1721

1822

@@ -91,6 +95,44 @@ def __getitem__(self, key: DeckLocation) -> Optional[DeckItem]:
9195

9296
return item
9397

98+
def __delitem__(self, key: DeckLocation) -> None:
99+
if self._api_version == APIVersion(2, 14):
100+
# __delitem__() support history:
101+
#
102+
# * PAPIv<=2.13 (non Protocol Engine): Yes, but that goes through a different Deck class
103+
# * PAPIv2.14 (Protocol Engine): No
104+
# * PAPIv2.15 (Protocol Engine): Yes
105+
raise APIVersionError(
106+
f"Deleting deck elements is not supported with apiLevel {self._api_version}."
107+
f" Try increasing your apiLevel to {APIVersion(2, 15)}."
108+
)
109+
110+
slot_name = _get_slot_name(
111+
key, self._api_version, self._protocol_core.robot_type
112+
)
113+
item_core = self._protocol_core.get_slot_item(slot_name)
114+
115+
if item_core is None:
116+
# No-op if trying to delete from an empty slot.
117+
# This matches pre-Protocol-Engine (PAPIv<=2.13) behavior.
118+
pass
119+
elif isinstance(item_core, AbstractModuleCore):
120+
# Protocol Engine does not support removing modules from the deck.
121+
# This is a change from pre-Protocol-Engine (PAPIv<=2.13) behavior, unfortunately.
122+
raise TypeError(
123+
f"Slot {repr(key)} contains a module, {item_core.get_display_name()}."
124+
f" You can only delete labware, not modules."
125+
)
126+
else:
127+
self._protocol_core.move_labware(
128+
item_core,
129+
new_location=OFF_DECK,
130+
use_gripper=False,
131+
pause_for_manual_move=False,
132+
pick_up_offset=None,
133+
drop_offset=None,
134+
)
135+
94136
def __iter__(self) -> Iterator[str]:
95137
"""Iterate through all deck slots."""
96138
return iter(self._slot_definitions_by_name)

api/src/opentrons/protocol_api/protocol_context.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -597,6 +597,7 @@ def move_labware(
597597
labware_core=labware._core,
598598
new_location=location,
599599
use_gripper=use_gripper,
600+
pause_for_manual_move=True,
600601
pick_up_offset=_pick_up_offset,
601602
drop_offset=_drop_offset,
602603
)

api/tests/opentrons/protocol_api/core/engine/test_protocol_core.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -546,10 +546,12 @@ def test_load_adapter(
546546

547547

548548
@pytest.mark.parametrize(
549-
argnames=["use_gripper", "expected_strategy"],
549+
argnames=["use_gripper", "pause_for_manual_move", "expected_strategy"],
550550
argvalues=[
551-
(True, LabwareMovementStrategy.USING_GRIPPER),
552-
(False, LabwareMovementStrategy.MANUAL_MOVE_WITH_PAUSE),
551+
(True, False, LabwareMovementStrategy.USING_GRIPPER),
552+
(True, True, LabwareMovementStrategy.USING_GRIPPER),
553+
(False, False, LabwareMovementStrategy.MANUAL_MOVE_WITHOUT_PAUSE),
554+
(False, True, LabwareMovementStrategy.MANUAL_MOVE_WITH_PAUSE),
553555
],
554556
)
555557
@pytest.mark.parametrize(
@@ -566,6 +568,7 @@ def test_move_labware(
566568
mock_engine_client: EngineClient,
567569
expected_strategy: LabwareMovementStrategy,
568570
use_gripper: bool,
571+
pause_for_manual_move: bool,
569572
pick_up_offset: Optional[Tuple[float, float, float]],
570573
drop_offset: Optional[Tuple[float, float, float]],
571574
) -> None:
@@ -580,6 +583,7 @@ def test_move_labware(
580583
labware_core=labware,
581584
new_location=DeckSlotName.SLOT_5,
582585
use_gripper=use_gripper,
586+
pause_for_manual_move=pause_for_manual_move,
583587
pick_up_offset=pick_up_offset,
584588
drop_offset=drop_offset,
585589
)
@@ -618,6 +622,7 @@ def test_move_labware_on_non_connected_module(
618622
labware_core=labware,
619623
new_location=non_connected_module_core,
620624
use_gripper=False,
625+
pause_for_manual_move=True,
621626
pick_up_offset=None,
622627
drop_offset=None,
623628
)
@@ -650,6 +655,7 @@ def test_move_labware_off_deck(
650655
labware_core=labware,
651656
new_location=OFF_DECK,
652657
use_gripper=False,
658+
pause_for_manual_move=True,
653659
pick_up_offset=None,
654660
drop_offset=None,
655661
)

api/tests/opentrons/protocol_api/test_deck.py

Lines changed: 91 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,15 @@
99

1010
from opentrons.motion_planning import adjacent_slots_getters as mock_adjacent_slots
1111
from opentrons.protocols.api_support.types import APIVersion
12-
from opentrons.protocol_api.core.common import ProtocolCore, LabwareCore
12+
from opentrons.protocols.api_support.util import APIVersionError
13+
from opentrons.protocol_api.core.common import ProtocolCore, LabwareCore, ModuleCore
1314
from opentrons.protocol_api.core.core_map import LoadedCoreMap
14-
from opentrons.protocol_api import Deck, Labware, validation as mock_validation
15+
from opentrons.protocol_api import (
16+
Deck,
17+
Labware,
18+
OFF_DECK,
19+
validation as mock_validation,
20+
)
1521
from opentrons.protocol_api.deck import CalibrationPosition
1622
from opentrons.types import DeckSlotName, Point
1723

@@ -135,6 +141,89 @@ def test_get_slot_item(
135141
assert subject[42] is mock_labware
136142

137143

144+
def test_delitem_aliases_to_move_labware(
145+
decoy: Decoy,
146+
mock_protocol_core: ProtocolCore,
147+
api_version: APIVersion,
148+
subject: Deck,
149+
) -> None:
150+
"""It should be equivalent to a manual labware move to off-deck, without pausing."""
151+
mock_labware_core = decoy.mock(cls=LabwareCore)
152+
153+
decoy.when(mock_protocol_core.robot_type).then_return("OT-3 Standard")
154+
decoy.when(
155+
mock_validation.ensure_and_convert_deck_slot(42, api_version, "OT-3 Standard")
156+
).then_return(DeckSlotName.SLOT_2)
157+
decoy.when(mock_protocol_core.get_slot_item(DeckSlotName.SLOT_2)).then_return(
158+
mock_labware_core
159+
)
160+
161+
del subject[42]
162+
163+
decoy.verify(
164+
mock_protocol_core.move_labware(
165+
mock_labware_core,
166+
OFF_DECK,
167+
use_gripper=False,
168+
pause_for_manual_move=False,
169+
pick_up_offset=None,
170+
drop_offset=None,
171+
)
172+
)
173+
174+
175+
@pytest.mark.parametrize("api_version", [APIVersion(2, 14)])
176+
def test_delitem_raises_on_api_2_14(
177+
subject: Deck,
178+
) -> None:
179+
"""It should raise on apiLevel 2.14."""
180+
with pytest.raises(APIVersionError):
181+
del subject[1]
182+
183+
184+
def test_delitem_noops_if_slot_is_empty(
185+
decoy: Decoy,
186+
mock_protocol_core: ProtocolCore,
187+
api_version: APIVersion,
188+
subject: Deck,
189+
) -> None:
190+
"""It should do nothing, and not raise anything, if you try to delete from an empty slot."""
191+
decoy.when(mock_protocol_core.robot_type).then_return("OT-3 Standard")
192+
decoy.when(
193+
mock_validation.ensure_and_convert_deck_slot(1, api_version, "OT-3 Standard")
194+
).then_return(DeckSlotName.SLOT_1)
195+
decoy.when(mock_protocol_core.get_slot_item(DeckSlotName.SLOT_1)).then_return(None)
196+
197+
del subject[1]
198+
199+
200+
def test_delitem_raises_if_slot_has_module(
201+
decoy: Decoy,
202+
mock_protocol_core: ProtocolCore,
203+
api_version: APIVersion,
204+
subject: Deck,
205+
) -> None:
206+
"""It should raise a descriptive error if you try to delete a module."""
207+
decoy.when(mock_protocol_core.robot_type).then_return("OT-3 Standard")
208+
mock_module_core = decoy.mock(cls=ModuleCore)
209+
decoy.when(mock_module_core.get_display_name()).then_return("<module display name>")
210+
decoy.when(
211+
mock_validation.ensure_and_convert_deck_slot(2, api_version, "OT-3 Standard")
212+
).then_return(DeckSlotName.SLOT_2)
213+
decoy.when(mock_protocol_core.get_slot_item(DeckSlotName.SLOT_2)).then_return(
214+
mock_module_core
215+
)
216+
217+
with pytest.raises(
218+
TypeError,
219+
match=(
220+
"Slot 2 contains a module, <module display name>."
221+
" You can only delete labware, not modules."
222+
),
223+
):
224+
del subject[2]
225+
226+
138227
@pytest.mark.parametrize(
139228
"deck_definition",
140229
[

api/tests/opentrons/protocol_api/test_protocol_context.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -517,6 +517,7 @@ def test_move_labware_to_slot(
517517
labware_core=mock_labware_core,
518518
new_location=DeckSlotName.SLOT_1,
519519
use_gripper=False,
520+
pause_for_manual_move=True,
520521
pick_up_offset=None,
521522
drop_offset=(1, 2, 3),
522523
)
@@ -556,6 +557,7 @@ def test_move_labware_to_module(
556557
labware_core=mock_labware_core,
557558
new_location=mock_module_core,
558559
use_gripper=False,
560+
pause_for_manual_move=True,
559561
pick_up_offset=None,
560562
drop_offset=None,
561563
)
@@ -586,6 +588,7 @@ def test_move_labware_off_deck(
586588
labware_core=mock_labware_core,
587589
new_location=OFF_DECK,
588590
use_gripper=False,
591+
pause_for_manual_move=True,
589592
pick_up_offset=None,
590593
drop_offset=None,
591594
)

0 commit comments

Comments
 (0)