Skip to content

Commit 7eb658b

Browse files
test(api): Catch labware versions not included in any Python Protocol API version (#19146)
## Overview When you add a new version of an existing labware definition, one big gotcha is that you need to remember to add it to `load_labware_params.py`, so new Python Protocol API `apiLevel`s will load it. If you forget to do this, Python protocols will continue to load the old labware version. There has never been anything to indicate that you need to do this, so it's been very easy to accidentally skip. This PR adds a test that will fail if a new labware version is added without accounting for it in `load_labware_params.py`. ## Changelog * Refactor `load_labware_params.py` to be more amenable to testing. * Add a test that fails if a new labware version is added without being added to any Python Protocol API `apiLevel`. * Hard-code a list of temporary exceptions. See the comments for details.
1 parent dcc8129 commit 7eb658b

File tree

5 files changed

+321
-129
lines changed

5 files changed

+321
-129
lines changed
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
"""The versions of standard labware that the Protocol API should load by default."""
2+
3+
from typing import TypeAlias
4+
from opentrons.protocols.api_support.types import APIVersion
5+
6+
7+
DefaultLabwareVersions: TypeAlias = dict[APIVersion, dict[str, int]]
8+
9+
10+
# This:
11+
#
12+
# {
13+
# APIVersion(2, 100): {
14+
# "foo_well_plate": 3,
15+
# },
16+
# APIVersion(2, 105): {
17+
# "foo_well_plate": 7
18+
# }
19+
# }
20+
#
21+
# Means this:
22+
#
23+
# apiLevels Load name Default labware version
24+
# ----------------------------------------------------------
25+
# <2.100 foo_well_plate 1
26+
# >=2.100,<2.105 foo_well_plate 3
27+
# >=2.105 foo_well_plate 7
28+
# [any] [anything else] 1
29+
DEFAULT_LABWARE_VERSIONS: DefaultLabwareVersions = {
30+
APIVersion(2, 14): {
31+
"armadillo_96_wellplate_200ul_pcr_full_skirt": 2,
32+
"biorad_384_wellplate_50ul": 2,
33+
"biorad_96_wellplate_200ul_pcr": 2,
34+
"corning_12_wellplate_6.9ml_flat": 2,
35+
"corning_24_wellplate_3.4ml_flat": 2,
36+
"corning_384_wellplate_112ul_flat": 2,
37+
"corning_48_wellplate_1.6ml_flat": 2,
38+
"corning_6_wellplate_16.8ml_flat": 2,
39+
"corning_96_wellplate_360ul_flat": 2,
40+
"nest_1_reservoir_195ml": 2,
41+
"nest_96_wellplate_100ul_pcr_full_skirt": 2,
42+
"nest_96_wellplate_200ul_flat": 2,
43+
"nest_96_wellplate_2ml_deep": 2,
44+
"opentrons_24_aluminumblock_generic_2ml_screwcap": 2,
45+
"opentrons_96_aluminumblock_generic_pcr_strip_200ul": 2,
46+
"opentrons_96_wellplate_200ul_pcr_full_skirt": 2,
47+
},
48+
APIVersion(2, 23): {
49+
"agilent_1_reservoir_290ml": 2,
50+
"appliedbiosystemsmicroamp_384_wellplate_40ul": 2,
51+
"armadillo_96_wellplate_200ul_pcr_full_skirt": 3,
52+
"axygen_1_reservoir_90ml": 2,
53+
"biorad_384_wellplate_50ul": 3,
54+
"biorad_96_wellplate_200ul_pcr": 3,
55+
"corning_12_wellplate_6.9ml_flat": 3,
56+
"corning_24_wellplate_3.4ml_flat": 3,
57+
"corning_384_wellplate_112ul_flat": 3,
58+
"corning_48_wellplate_1.6ml_flat": 3,
59+
"corning_6_wellplate_16.8ml_flat": 3,
60+
"corning_96_wellplate_360ul_flat": 3,
61+
"nest_12_reservoir_15ml": 2,
62+
"nest_1_reservoir_195ml": 3,
63+
"nest_1_reservoir_290ml": 2,
64+
"nest_96_wellplate_100ul_pcr_full_skirt": 3,
65+
"nest_96_wellplate_200ul_flat": 3,
66+
"nest_96_wellplate_2ml_deep": 3,
67+
"opentrons_10_tuberack_falcon_4x50ml_6x15ml_conical": 2,
68+
"opentrons_10_tuberack_nest_4x50ml_6x15ml_conical": 2,
69+
"opentrons_15_tuberack_falcon_15ml_conical": 2,
70+
"opentrons_15_tuberack_nest_15ml_conical": 2,
71+
"opentrons_24_aluminumblock_generic_2ml_screwcap": 3,
72+
"opentrons_24_aluminumblock_nest_0.5ml_screwcap": 2,
73+
"opentrons_24_aluminumblock_nest_1.5ml_screwcap": 2,
74+
"opentrons_24_aluminumblock_nest_1.5ml_snapcap": 2,
75+
"opentrons_24_aluminumblock_nest_2ml_screwcap": 2,
76+
"opentrons_24_aluminumblock_nest_2ml_snapcap": 2,
77+
"opentrons_24_tuberack_eppendorf_1.5ml_safelock_snapcap": 2,
78+
"opentrons_24_tuberack_eppendorf_2ml_safelock_snapcap": 2,
79+
"opentrons_24_tuberack_generic_2ml_screwcap": 2,
80+
"opentrons_24_tuberack_nest_0.5ml_screwcap": 2,
81+
"opentrons_24_tuberack_nest_1.5ml_screwcap": 2,
82+
"opentrons_24_tuberack_nest_1.5ml_snapcap": 2,
83+
"opentrons_24_tuberack_nest_2ml_screwcap": 2,
84+
"opentrons_24_tuberack_nest_2ml_snapcap": 2,
85+
"opentrons_6_tuberack_falcon_50ml_conical": 2,
86+
"opentrons_6_tuberack_nest_50ml_conical": 2,
87+
"opentrons_96_aluminumblock_generic_pcr_strip_200ul": 3,
88+
"opentrons_96_wellplate_200ul_pcr_full_skirt": 3,
89+
"opentrons_tough_pcr_auto_sealing_lid": 2,
90+
"thermoscientificnunc_96_wellplate_1300ul": 2,
91+
"thermoscientificnunc_96_wellplate_2000ul": 2,
92+
"usascientific_12_reservoir_22ml": 2,
93+
"usascientific_96_wellplate_2.4ml_deep": 2,
94+
},
95+
APIVersion(2, 25): {
96+
"appliedbiosystemsmicroamp_384_wellplate_40ul": 3,
97+
"axygen_96_wellplate_500ul": 2,
98+
"biorad_384_wellplate_50ul": 4,
99+
"biorad_96_wellplate_200ul_pcr": 4,
100+
"corning_12_wellplate_6.9ml_flat": 4,
101+
"corning_24_wellplate_3.4ml_flat": 4,
102+
"corning_48_wellplate_1.6ml_flat": 5,
103+
"corning_6_wellplate_16.8ml_flat": 4,
104+
"corning_96_wellplate_360ul_flat": 4,
105+
"ibidi_96_square_well_plate_300ul": 2,
106+
"nest_96_wellplate_100ul_pcr_full_skirt": 4,
107+
"nest_96_wellplate_200ul_flat": 4,
108+
"nest_96_wellplate_2ml_deep": 4,
109+
"opentrons_96_wellplate_200ul_pcr_full_skirt": 4,
110+
"smc_384_read_plate": 2,
111+
"thermoscientificnunc_96_wellplate_1300ul": 3,
112+
"thermoscientificnunc_96_wellplate_2000ul": 3,
113+
"usascientific_96_wellplate_2.4ml_deep": 3,
114+
},
115+
}
116+
117+
118+
# Labware where, for whatever reason, we don't want `opentrons.protocol_api` to load
119+
# the latest available version.
120+
#
121+
# Typically, this is because the latest available version of the labware is some kind of
122+
# unpublicized draft.
123+
#
124+
# Beware, though, that users can still load the unpublicized draft if they know how, e.g.
125+
# by passing an explicit `version` arg to `ProtocolContext.load_labware()`.
126+
# And non-`opentrons.protocol_api` code like Labware Library, Protocol Designer, and
127+
# Quick Transfer will still use the unpublicized draft unless you exclude it through
128+
# other means.
129+
#
130+
# This list should not be consumed by production code--it's only for the benefit of tests
131+
# that make sure every labware is accounted for somehow.
132+
KNOWN_EXCEPTIONS_FOR_TESTS: set[str] = {
133+
# Dev testing junk for labware schema 3, not things that users should ever load:
134+
"schema3test_96_well_aluminum_block",
135+
"schema3test_96_wellplate_200ul_pcr_full_skirt",
136+
"schema3test_aluminum_flat_bottom_plate",
137+
"schema3test_flex_96_tiprack_200ul",
138+
"schema3test_flex_96_tiprack_adapter",
139+
"schema3test_flex_tiprack_lid",
140+
"schema3test_tough_pcr_auto_sealing_lid",
141+
"schema3test_universal_flat_adapter",
142+
# These were supposed to be short-lived drafts as part of of a one-two punch of
143+
# https://github.com/Opentrons/opentrons/pull/18266 + https://github.com/Opentrons/opentrons/pull/18284,
144+
# but the second punch took a while. We should merge the second punch after v8.6.0
145+
# and remove these exceptions as part of that.
146+
"agilent_1_reservoir_290ml",
147+
"corning_384_wellplate_112ul_flat",
148+
"nest_1_reservoir_290ml",
149+
"opentrons_24_aluminumblock_nest_0.5ml_screwcap",
150+
"opentrons_24_tuberack_nest_0.5ml_screwcap",
151+
"opentrons_96_aluminumblock_generic_pcr_strip_200ul",
152+
"usascientific_12_reservoir_22ml",
153+
}
154+
155+
156+
def get_standard_labware_default_version(
157+
api_version: APIVersion,
158+
load_name: str,
159+
default_labware_versions: DefaultLabwareVersions = DEFAULT_LABWARE_VERSIONS,
160+
) -> int:
161+
"""Return what version of a standard labware the Protocol API should load by default.
162+
163+
The `default_labware_versions` param is exposed for testability and should be left
164+
unspecified.
165+
"""
166+
default_labware_versions_newest_to_oldest = sorted(
167+
default_labware_versions.items(), key=lambda kv: kv[0], reverse=True
168+
)
169+
for (
170+
breakpoint_api_version,
171+
breakpoint_labware_versions,
172+
) in default_labware_versions_newest_to_oldest:
173+
if (
174+
api_version >= breakpoint_api_version
175+
and load_name in breakpoint_labware_versions
176+
):
177+
return breakpoint_labware_versions[load_name]
178+
179+
return 1
Lines changed: 4 additions & 127 deletions
Original file line numberDiff line numberDiff line change
@@ -1,115 +1,11 @@
1-
from copy import deepcopy
2-
1+
from opentrons.protocol_api.core.engine._default_labware_versions import (
2+
get_standard_labware_default_version,
3+
)
34
from opentrons.protocols.api_support.constants import OPENTRONS_NAMESPACE
45
from opentrons.protocol_engine.state.labware import LabwareLoadParams
56
from opentrons.protocols.api_support.types import APIVersion
67

78

8-
# Default versions of Opentrons standard labware definitions in Python Protocol API
9-
# v2.14 and above. Labware not explicitly listed here default to 1.
10-
#
11-
# TODO(jbl 2023-08-01) this needs to be done more holistically, both to find the version and make sure that
12-
# it corresponds to the API level is was released with
13-
_APILEVEL_2_14_OT_DEFAULT_VERSIONS: dict[str, int] = {
14-
# v1 of many labware definitions have wrong `zDimension`s. (Jira RSS-202.)
15-
# For "opentrons_96_aluminumblock_generic_pcr_strip_200ul" and
16-
# "opentrons_24_aluminumblock_generic_2ml_screwcap", they're wrong enough to
17-
# easily cause collisions. (Jira RSS-197.)
18-
"opentrons_24_aluminumblock_generic_2ml_screwcap": 2,
19-
"opentrons_96_aluminumblock_generic_pcr_strip_200ul": 2,
20-
# The following labware definitions have had a version bump due to using new properties
21-
# introduced in an inplace schema v2 update
22-
"armadillo_96_wellplate_200ul_pcr_full_skirt": 2,
23-
"biorad_96_wellplate_200ul_pcr": 2,
24-
"biorad_384_wellplate_50ul": 2,
25-
"corning_12_wellplate_6.9ml_flat": 2,
26-
"corning_384_wellplate_112ul_flat": 2,
27-
"corning_48_wellplate_1.6ml_flat": 2,
28-
"corning_96_wellplate_360ul_flat": 2,
29-
"nest_1_reservoir_195ml": 2,
30-
"nest_96_wellplate_100ul_pcr_full_skirt": 2,
31-
"nest_96_wellplate_200ul_flat": 2,
32-
"nest_96_wellplate_2ml_deep": 2,
33-
"opentrons_96_wellplate_200ul_pcr_full_skirt": 2,
34-
"corning_6_wellplate_16.8ml_flat": 2,
35-
"corning_24_wellplate_3.4ml_flat": 2,
36-
}
37-
38-
_APILEVEL_2_23_OT_DEFAULT_VERSIONS: dict[str, int] = {
39-
"agilent_1_reservoir_290ml": 2,
40-
"appliedbiosystemsmicroamp_384_wellplate_40ul": 2,
41-
"armadillo_96_wellplate_200ul_pcr_full_skirt": 3,
42-
"axygen_1_reservoir_90ml": 2,
43-
"biorad_384_wellplate_50ul": 3,
44-
"biorad_96_wellplate_200ul_pcr": 3,
45-
"corning_12_wellplate_6.9ml_flat": 3,
46-
"corning_24_wellplate_3.4ml_flat": 3,
47-
"corning_384_wellplate_112ul_flat": 3,
48-
"corning_48_wellplate_1.6ml_flat": 3,
49-
"corning_6_wellplate_16.8ml_flat": 3,
50-
"corning_96_wellplate_360ul_flat": 3,
51-
"nest_12_reservoir_15ml": 2,
52-
"nest_1_reservoir_195ml": 3,
53-
"nest_1_reservoir_290ml": 2,
54-
"nest_96_wellplate_100ul_pcr_full_skirt": 3,
55-
"nest_96_wellplate_200ul_flat": 3,
56-
"nest_96_wellplate_2ml_deep": 3,
57-
"opentrons_10_tuberack_falcon_4x50ml_6x15ml_conical": 2,
58-
"opentrons_10_tuberack_nest_4x50ml_6x15ml_conical": 2,
59-
"opentrons_15_tuberack_falcon_15ml_conical": 2,
60-
"opentrons_15_tuberack_nest_15ml_conical": 2,
61-
"opentrons_24_aluminumblock_generic_2ml_screwcap": 3,
62-
"opentrons_24_aluminumblock_nest_0.5ml_screwcap": 2,
63-
"opentrons_24_aluminumblock_nest_1.5ml_screwcap": 2,
64-
"opentrons_24_aluminumblock_nest_1.5ml_snapcap": 2,
65-
"opentrons_24_aluminumblock_nest_2ml_screwcap": 2,
66-
"opentrons_24_aluminumblock_nest_2ml_snapcap": 2,
67-
"opentrons_24_tuberack_eppendorf_1.5ml_safelock_snapcap": 2,
68-
"opentrons_24_tuberack_eppendorf_2ml_safelock_snapcap": 2,
69-
"opentrons_24_tuberack_generic_2ml_screwcap": 2,
70-
"opentrons_24_tuberack_nest_0.5ml_screwcap": 2,
71-
"opentrons_24_tuberack_nest_1.5ml_screwcap": 2,
72-
"opentrons_24_tuberack_nest_1.5ml_snapcap": 2,
73-
"opentrons_24_tuberack_nest_2ml_screwcap": 2,
74-
"opentrons_24_tuberack_nest_2ml_snapcap": 2,
75-
"opentrons_6_tuberack_falcon_50ml_conical": 2,
76-
"opentrons_6_tuberack_nest_50ml_conical": 2,
77-
"opentrons_96_aluminumblock_generic_pcr_strip_200ul": 3,
78-
"opentrons_96_wellplate_200ul_pcr_full_skirt": 3,
79-
"opentrons_tough_pcr_auto_sealing_lid": 2,
80-
"thermoscientificnunc_96_wellplate_1300ul": 2,
81-
"thermoscientificnunc_96_wellplate_2000ul": 2,
82-
"usascientific_12_reservoir_22ml": 2,
83-
"usascientific_96_wellplate_2.4ml_deep": 2,
84-
}
85-
86-
_APILEVEL_2_25_OT_DEFAULT_VERSIONS: dict[str, int] = deepcopy(
87-
_APILEVEL_2_23_OT_DEFAULT_VERSIONS
88-
)
89-
_APILEVEL_2_25_OT_DEFAULT_VERSIONS.update(
90-
{
91-
"appliedbiosystemsmicroamp_384_wellplate_40ul": 3,
92-
"axygen_96_wellplate_500ul": 2,
93-
"biorad_384_wellplate_50ul": 4,
94-
"biorad_96_wellplate_200ul_pcr": 4,
95-
"corning_12_wellplate_6.9ml_flat": 4,
96-
"corning_24_wellplate_3.4ml_flat": 4,
97-
"corning_48_wellplate_1.6ml_flat": 5,
98-
"corning_6_wellplate_16.8ml_flat": 4,
99-
"corning_96_wellplate_360ul_flat": 4,
100-
"ibidi_96_square_well_plate_300ul": 2,
101-
"nest_96_wellplate_100ul_pcr_full_skirt": 4,
102-
"nest_96_wellplate_200ul_flat": 4,
103-
"nest_96_wellplate_2ml_deep": 4,
104-
"opentrons_96_wellplate_200ul_pcr_full_skirt": 4,
105-
"smc_384_read_plate": 2,
106-
"thermoscientificnunc_96_wellplate_1300ul": 3,
107-
"thermoscientificnunc_96_wellplate_2000ul": 3,
108-
"usascientific_96_wellplate_2.4ml_deep": 3,
109-
}
110-
)
111-
112-
1139
class AmbiguousLoadLabwareParamsError(RuntimeError):
11410
"""Error raised when specific labware parameters cannot be found due to multiple matching labware definitions."""
11511

@@ -158,7 +54,7 @@ def matches_params(custom_params: LabwareLoadParams) -> bool:
15854
resolved_version = (
15955
version
16056
if version is not None
161-
else _get_default_version_for_standard_labware(
57+
else get_standard_labware_default_version(
16258
load_name=load_name, api_version=api_version
16359
)
16460
)
@@ -175,22 +71,3 @@ def matches_params(custom_params: LabwareLoadParams) -> bool:
17571
resolved_version = filtered_custom_params[0].version
17672

17773
return resolved_namespace, resolved_version
178-
179-
180-
def _get_default_version_for_standard_labware(
181-
load_name: str, api_version: APIVersion
182-
) -> int:
183-
# We know the protocol is running at least apiLevel 2.14 by this point because
184-
# apiLevel 2.13 and below has its own separate code path for resolving labware.
185-
if (
186-
api_version >= APIVersion(2, 25)
187-
and load_name in _APILEVEL_2_25_OT_DEFAULT_VERSIONS
188-
):
189-
return _APILEVEL_2_25_OT_DEFAULT_VERSIONS[load_name]
190-
elif (
191-
api_version >= APIVersion(2, 23)
192-
and load_name in _APILEVEL_2_23_OT_DEFAULT_VERSIONS
193-
):
194-
return _APILEVEL_2_23_OT_DEFAULT_VERSIONS[load_name]
195-
else:
196-
return _APILEVEL_2_14_OT_DEFAULT_VERSIONS.get(load_name, 1)

0 commit comments

Comments
 (0)