Skip to content

Commit

Permalink
first pass at parsing and using parameters in python protocols
Browse files Browse the repository at this point in the history
  • Loading branch information
jbleon95 committed Mar 13, 2024
1 parent 12f39c6 commit dc0386c
Show file tree
Hide file tree
Showing 5 changed files with 115 additions and 2 deletions.
20 changes: 20 additions & 0 deletions api/src/opentrons/protocol_api/parameters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from typing import Dict, Optional

from opentrons.protocol_reader.parameter_definition import AllowedTypes


class Parameters:
def __init__(self, parameters: Optional[Dict[str, AllowedTypes]] = None) -> None:
self._values: Dict[str, AllowedTypes] = {}
if parameters is not None:
for name, value in parameters.items():
self._set_parameter(name, value)

def _set_parameter(self, variable_name: str, value: AllowedTypes) -> None:
# TODO raise an error if the variable name already exists to prevent overwriting anything important
if not hasattr(self, variable_name):
setattr(self, variable_name, value)
self._values[variable_name] = value

def get_all(self) -> Dict[str, AllowedTypes]:
return self._values
6 changes: 6 additions & 0 deletions api/src/opentrons/protocol_api/protocol_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
MagneticBlockContext,
ModuleContext,
)
from .parameters import Parameters


logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -167,6 +168,7 @@ def __init__(
self._core.load_ot2_fixed_trash_bin()

self._commands: List[str] = []
self._params: Parameters = Parameters()
self._unsubscribe_commands: Optional[Callable[[], None]] = None
self.clear_commands()

Expand Down Expand Up @@ -215,6 +217,10 @@ def bundled_data(self) -> Dict[str, bytes]:
"""
return self._bundled_data

@property
def params(self) -> Parameters:
return self._params

def cleanup(self) -> None:
"""Finalize and clean up the protocol context."""
if self._unsubscribe_commands:
Expand Down
9 changes: 7 additions & 2 deletions api/src/opentrons/protocol_reader/parameter_definition.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ def __init__(
Arguments:
display_name: The display name of the parameter as it would show up on the frontend.
variable_name: The variable name the protocol will be referred to in the run context.
variable_name: The variable name the parameter will be referred to in the run context.
parameter_type: Can be bool, int, float or str. Must match the type of default and all choices or
min and max values
default: The default value the parameter is set to. This will be used in initial analysis.
Expand Down Expand Up @@ -153,7 +153,7 @@ def __init__(
self._maximum = maximum

self._default: ParamType = default
self._value: ParamType = default
self.value: ParamType = default

@property
def value(self) -> ParamType:
Expand All @@ -180,3 +180,8 @@ def value(self, new_value: ParamType) -> None:
f"Parameter must be between {self._minimum} and {self._maximum} inclusive."
)
self._value = new_value

@property
def variable_name(self) -> str:
"""The in-protocol variable name of the parameter."""
return self._variable_name
47 changes: 47 additions & 0 deletions api/src/opentrons/protocol_reader/parameter_parser.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
"""Parameter parser for python protocols."""

from typing import List, Dict

from .parameter_definition import ParameterDefinition, AllowedTypes


class ParameterParser:
"""Parser for parameters defined in a python protocol."""

def __init__(self) -> None:
"""Initializes a parser for user-set parameters."""
self._parameters: List[ParameterDefinition[AllowedTypes]] = []

def add_int(
self,
display_name: str,
variable_name: str,
default: int,
minimum: int,
maximum: int,
) -> None:
"""Creates an integer parameter, settable within a given range.
Arguments:
display_name: The display name of the int parameter as it would show up on the frontend.
variable_name: The variable name the int parameter will be referred to in the run context.
default: The default value the int parameter will be set to. This will be used in initial analysis.
minimum: The minimum value the int parameter can be set to (inclusive).
maximum: The maximum value the int parameter can be set to (inclusive).
"""
self._parameters.append(
ParameterDefinition(
parameter_type=int,
display_name=display_name,
variable_name=variable_name,
default=default,
minimum=minimum,
maximum=maximum,
)
)

def get_variable_names_and_values(self) -> Dict[str, AllowedTypes]:
"""Returns all parameters in a dictionary with the variable name as the key and current value as the value."""
return {
parameter.variable_name: parameter.value for parameter in self._parameters
}
35 changes: 35 additions & 0 deletions api/src/opentrons/protocols/execution/execute_python.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,12 @@

from opentrons.drivers.smoothie_drivers.errors import SmoothieAlarm
from opentrons.protocol_api import ProtocolContext
from opentrons.protocol_api.parameters import Parameters
from opentrons.protocol_reader.parameter_parser import ParameterParser
from opentrons.protocols.execution.errors import ExceptionInProtocolError
from opentrons.protocols.types import PythonProtocol, MalformedPythonProtocolError


from opentrons_shared_data.errors.exceptions import ExecutionCancelledError

MODULE_LOG = logging.getLogger(__name__)
Expand All @@ -29,6 +33,14 @@ def _runfunc_ok(run_func: Any):
)


def _add_parameters_func_ok(add_parameters_func: Any) -> None:
if not callable(add_parameters_func):
raise SyntaxError("'add_parameters' must be a function.")
sig = inspect.Signature.from_callable(add_parameters_func)
if len(sig.parameters) != 1:
raise SyntaxError("Function 'add_parameters' must take exactly one argument.")


def _find_protocol_error(tb, proto_name):
"""Return the FrameInfo for the lowest frame in the traceback from the
protocol.
Expand All @@ -41,6 +53,26 @@ def _find_protocol_error(tb, proto_name):
raise KeyError


def _parse_and_set_parameters(new_globs: Dict[Any, Any], filename: str) -> Parameters:
try:
_add_parameters_func_ok(new_globs.get("add_parameters"))
except SyntaxError as se:
raise MalformedPythonProtocolError(str(se))
parser = ParameterParser()
new_globs["__parser"] = parser
try:
exec("add_parameters(__parser)", new_globs)
except Exception as e:
exc_type, exc_value, tb = sys.exc_info()
try:
frame = _find_protocol_error(tb, filename)
except KeyError:
# No pretty names, just raise it
raise e
raise ExceptionInProtocolError(e, tb, str(e), frame.lineno) from e
return Parameters(parameters=parser.get_variable_names_and_values())


def run_python(proto: PythonProtocol, context: ProtocolContext):
new_globs: Dict[Any, Any] = {}
exec(proto.contents, new_globs)
Expand All @@ -60,6 +92,9 @@ def run_python(proto: PythonProtocol, context: ProtocolContext):
# AST filename.
filename = proto.filename or "<protocol>"

if new_globs.get("add_parameters"):
context._params = _parse_and_set_parameters(new_globs, filename)

try:
_runfunc_ok(new_globs.get("run"))
except SyntaxError as se:
Expand Down

0 comments on commit dc0386c

Please sign in to comment.