def test_proto_with_exception(loop): ctx = ProtocolContext(loop) exc_in_root = '''metadata={"apiLevel": "2.0"} def run(ctx): raise Exception("hi") ''' protocol = parse(exc_in_root) with pytest.raises(execute_python.ExceptionInProtocolError) as e: execute.run_protocol(protocol, context=ctx) assert 'Exception [line 4]: hi' in str(e.value) nested_exc = ''' import ast def this_throws(): raise Exception("hi") def run(ctx): this_throws() metadata={"apiLevel": "2.0"}; ''' protocol = parse(nested_exc) with pytest.raises(execute_python.ExceptionInProtocolError) as e: execute.run_protocol(protocol, context=ctx) assert '[line 5]' in str(e.value) assert 'Exception [line 5]: hi' in str(e.value)
def test_proto_with_exception(ensure_api2, loop): ctx = ProtocolContext(loop) exc_in_root = ''' def run(ctx): raise Exception("hi") ''' protocol = parse(exc_in_root) with pytest.raises(execute.ExceptionInProtocolError) as e: execute.run_protocol(protocol, context=ctx) assert 'Exception [line 3]: hi' in str(e.value) nested_exc = ''' import ast def this_throws(): raise Exception("hi") def run(ctx): this_throws() ''' protocol = parse(nested_exc) with pytest.raises(execute.ExceptionInProtocolError) as e: execute.run_protocol(protocol, context=ctx) assert '[line 5]' in str(e.value) assert 'Exception [line 5]: hi' in str(e.value)
def test_bad_protocol(loop): ctx = ProtocolContext(loop) no_run = parse(''' metadata={"apiLevel": "2.0"} print("hi") ''') with pytest.raises(execute.MalformedProtocolError) as e: execute.run_protocol(no_run, context=ctx) assert "No function 'run" in str(e.value) no_args = parse(''' metadata={"apiLevel": "2.0"} def run(): pass ''') with pytest.raises(execute.MalformedProtocolError) as e: execute.run_protocol(no_args, context=ctx) assert "Function 'run()' does not take any parameters" in str(e.value) many_args = parse(''' metadata={"apiLevel": "2.0"} def run(a, b): pass ''') with pytest.raises(execute.MalformedProtocolError) as e: execute.run_protocol(many_args, context=ctx) assert "must be called with more than one argument" in str(e.value)
def test_parse_python_details( protocol, protocol_text_kind, filename, protocol_file): if protocol_text_kind == 'bytes': text = protocol.text.encode('utf-8') else: text = protocol.text if filename == 'real': fake_fname = protocol.filename else: fake_fname = None parsed = parse(text, fake_fname) assert isinstance(parsed, PythonProtocol) assert parsed.text == protocol.text assert isinstance(parsed.text, str) version = '2' if '2' in protocol.filename else '1' assert parsed.api_level == version fname = fake_fname if fake_fname else '<protocol>' assert parsed.filename == fname assert parsed.metadata == { 'protocolName': 'Testosaur', 'author': 'Opentrons <*****@*****.**>', 'description': 'A variant on "Dinosaur" for testing', 'source': 'Opentrons Repository' } assert parsed.contents == compile( protocol.text, filename=fname, mode='exec')
def test_papi_execute_json_v3(monkeypatch, loop, get_json_protocol_fixture): protocol_data = get_json_protocol_fixture( '3', 'testAllAtomicSingleV3', False) protocol = parse(protocol_data, None) ctx = ProtocolContext(loop=loop) ctx.home() # Check that we end up executing the protocol ok execute.run_protocol(protocol, True, ctx)
def test_papi_execute_json_v4(monkeypatch, loop, get_json_protocol_fixture): protocol_data = get_json_protocol_fixture('4', 'testModulesProtocol', False) protocol = parse(protocol_data, None) ctx = ProtocolContext(loop=loop) ctx.home() # Check that we end up executing the protocol ok execute.run_protocol(protocol, ctx)
def build_and_prep( cls, name, contents, hardware, loop, broker, motion_lock, extra_labware ): protocol = parse(contents, filename=name, extra_labware={labware.uri_from_definition(defn): defn for defn in extra_labware}) sess = cls(name, protocol, hardware, loop, broker, motion_lock) sess.prepare() return sess
def test_extra_contents(get_labware_fixture, protocol_file, protocol): fixture_96_plate = get_labware_fixture('fixture_96_plate') bundled_labware = {'fixture/fixture_96_plate/1': fixture_96_plate} extra_data = {'hi': b'there'} parsed = parse(protocol.text, 'testosaur.py', extra_labware=bundled_labware, extra_data=extra_data) assert parsed.extra_labware == bundled_labware assert parsed.bundled_data == extra_data
def test_parse_bundle_details(get_bundle_fixture): fixture = get_bundle_fixture('simple_bundle') filename = fixture['filename'] parsed = parse(fixture['binary_zipfile'], filename) assert isinstance(parsed, PythonProtocol) assert parsed.filename == 'protocol.ot2.py' assert parsed.bundled_labware == fixture['bundled_labware'] assert parsed.bundled_python == fixture['bundled_python'] assert parsed.bundled_data == fixture['bundled_data'] assert parsed.metadata == fixture['metadata'] assert parsed.api_level == APIVersion(2, 0)
def test_parse_json_details(get_json_protocol_fixture, protocol_details, protocol_text_kind, filename): protocol = get_json_protocol_fixture(*protocol_details, decode=False) if protocol_text_kind == 'text': protocol_text = protocol else: protocol_text = protocol.encode('utf-8') if filename == 'real': fname = 'simple.json' else: fname = None parsed = parse(protocol_text, fname) assert isinstance(parsed, JsonProtocol) assert parsed.filename == fname assert parsed.contents == json.loads(protocol) parsed.schema_version == int(protocol_details[0])
def test_bad_structure(bad_protocol): with pytest.raises(MalformedProtocolError): parse(bad_protocol)
def build_and_prep(cls, name, contents, hardware, loop, broker, motion_lock): protocol = parse(contents, filename=name) sess = cls(name, protocol, hardware, loop, broker, motion_lock) sess.prepare() return sess
def simulate(protocol_file: TextIO, propagate_logs=False, log_level='warning') -> List[Mapping[str, Any]]: """ Simulate the protocol itself. This is a one-stop function to simulate a protocol, whether python or json, no matter the api version, from external (i.e. not bound up in other internal server infrastructure) sources. To simulate an opentrons protocol from other places, pass in a file like object as protocol_file; this function either returns (if the simulation has no problems) or raises an exception. To call from the command line use either the autogenerated entrypoint ``opentrons_simulate`` (``opentrons_simulate.exe``, on windows) or ``python -m opentrons.simulate``. The return value is the run log, a list of dicts that represent the commands executed by the robot. Each dict has the following keys: - ``level``: The depth at which this command is nested - if this an aspirate inside a mix inside a transfer, for instance, it would be 3. - ``payload``: The command, its arguments, and how to format its text. For more specific details see :py:mod:`opentrons.commands`. To format a message from a payload do ``payload['text'].format(**payload)``. - ``logs``: Any log messages that occurred during execution of this command, as a logging.LogRecord :param file-like protocol_file: The protocol file to simulate. :param propagate_logs: Whether this function should allow logs from the Opentrons stack to propagate up to the root handler. This can be useful if you're integrating this function in a larger application, but most logs that occur during protocol simulation are best associated with the actions in the protocol that cause them. Default: ``False`` :type propagate_logs: bool :param log_level: The level of logs to capture in the runlog. Default: ``'warning'`` :type log_level: 'debug', 'info', 'warning', or 'error' :returns List[Dict[str, Dict[str, Any]]]: A run log for user output. """ stack_logger = logging.getLogger('opentrons') stack_logger.propagate = propagate_logs contents = protocol_file.read() protocol = parse.parse(contents, protocol_file.name) if opentrons.config.feature_flags.use_protocol_api_v2(): context = opentrons.protocol_api.contexts.ProtocolContext( bundled_labware=getattr(protocol, 'bundled_labware', None), bundled_data=getattr(protocol, 'bundled_data', None)) context.home() scraper = CommandScraper(stack_logger, log_level, context.broker) opentrons.protocol_api.execute.run_protocol(protocol, simulate=True, context=context) else: opentrons.robot.disconnect() scraper = CommandScraper(stack_logger, log_level, opentrons.robot.broker) if isinstance(protocol, JsonProtocol): opentrons.legacy_api.protocols.execute_protocol(protocol) else: exec(protocol.contents, {}) return scraper.commands
def execute(protocol_file: TextIO, propagate_logs: bool = False, log_level: str = 'warning', emit_runlog: Callable[[Dict[str, Any]], None] = None): """ Run the protocol itself. This is a one-stop function to run a protocol, whether python or json, no matter the api verson, from external (i.e. not bound up in other internal server infrastructure) sources. To run an opentrons protocol from other places, pass in a file like object as protocol_file; this function either returns (if the run has no problems) or raises an exception. To call from the command line use either the autogenerated entrypoint ``opentrons_execute`` or ``python -m opentrons.execute``. If the protocol is using Opentrons Protocol API V1, it does not need to explicitly call :py:meth:`.Robot.connect` or :py:meth:`.Robot.discover_modules`, or :py:meth:`.Robot.cache_instrument_models`. :param file-like protocol_file: The protocol file to execute :param propagate_logs: Whether this function should allow logs from the Opentrons stack to propagate up to the root handler. This can be useful if you're integrating this function in a larger application, but most logs that occur during protocol simulation are best associated with the actions in the protocol that cause them. Default: ``False`` :type propagate_logs: bool :param log_level: The level of logs to emit on the command line.. Default: 'warning' :type log_level: 'debug', 'info', 'warning', or 'error' :param emit_runlog: A callback for printing the runlog. If specified, this will be called whenever a command adds an entry to the runlog, which can be used for display and progress estimation. If specified, the callback should take a single argument (the name doesn't matter) which will be a dictionary (see below). Default: ``None`` The format of the runlog entries is as follows: .. code-block:: python { 'name': command_name, 'payload': { 'text': string_command_text, # The rest of this struct is command-dependent; see # opentrons.commands.commands. Its keys match format # keys in 'text', so that # entry['payload']['text'].format(**entry['payload']) # will produce a string with information filled in } } """ stack_logger = logging.getLogger('opentrons') stack_logger.propagate = propagate_logs stack_logger.setLevel(getattr(logging, log_level.upper(), logging.WARNING)) contents = protocol_file.read() protocol = parse(contents, protocol_file.name) if ff.use_protocol_api_v2(): context = get_protocol_api( bundled_labware=getattr(protocol, 'bundled_labware', None), bundled_data=getattr(protocol, 'bundled_data', None)) if emit_runlog: context.broker.subscribe(commands.command_types.COMMAND, emit_runlog) context.home() execute_apiv2.run_protocol(protocol, simulate=False, context=context) else: robot.connect() robot.cache_instrument_models() robot.discover_modules() robot.home() if emit_runlog: robot.broker.subscribe(commands.command_types.COMMAND, emit_runlog) if isinstance(protocol, JsonProtocol): legacy_api.protocols.execute_protocol(protocol) else: exec(protocol.contents, {})
def test_get_version(proto, version): parsed = parse(proto) assert parsed.api_level == version
def simulate( protocol_file: TextIO, file_name: str = None, custom_labware_paths: List[str] = None, custom_data_paths: List[str] = None, propagate_logs: bool = False, hardware_simulator_file_path: str = None, log_level: str = 'warning' ) -> Tuple[List[Mapping[str, Any]], Optional[BundleContents]]: """ Simulate the protocol itself. This is a one-stop function to simulate a protocol, whether python or json, no matter the api version, from external (i.e. not bound up in other internal server infrastructure) sources. To simulate an opentrons protocol from other places, pass in a file like object as protocol_file; this function either returns (if the simulation has no problems) or raises an exception. To call from the command line use either the autogenerated entrypoint ``opentrons_simulate`` (``opentrons_simulate.exe``, on windows) or ``python -m opentrons.simulate``. The return value is the run log, a list of dicts that represent the commands executed by the robot; and either the contents of the protocol that would be required to bundle, or ``None``. Each dict element in the run log has the following keys: - ``level``: The depth at which this command is nested - if this an aspirate inside a mix inside a transfer, for instance, it would be 3. - ``payload``: The command, its arguments, and how to format its text. For more specific details see :py:mod:`opentrons.commands`. To format a message from a payload do ``payload['text'].format(**payload)``. - ``logs``: Any log messages that occurred during execution of this command, as a logging.LogRecord :param file-like protocol_file: The protocol file to simulate. :param str file_name: The name of the file :param custom_labware_paths: A list of directories to search for custom labware, or None. Ignored if the apiv2 feature flag is not set. Loads valid labware from these paths and makes them available to the protocol context. :param custom_data_paths: A list of directories or files to load custom data files from. Ignored if the apiv2 feature flag if not set. Entries may be either files or directories. Specified files and the non-recursive contents of specified directories are presented by the protocol context in :py:attr:`.ProtocolContext.bundled_data`. :param hardware_simulator_file_path: A path to a JSON file defining a hardware simulator. :param propagate_logs: Whether this function should allow logs from the Opentrons stack to propagate up to the root handler. This can be useful if you're integrating this function in a larger application, but most logs that occur during protocol simulation are best associated with the actions in the protocol that cause them. Default: ``False`` :type propagate_logs: bool :param log_level: The level of logs to capture in the runlog. Default: ``'warning'`` :type log_level: 'debug', 'info', 'warning', or 'error' :returns: A tuple of a run log for user output, and possibly the required data to write to a bundle to bundle this protocol. The bundle is only emitted if bundling is allowed (see :py:meth:`allow_bundling`) and this is an unbundled Protocol API v2 python protocol. In other cases it is None. """ stack_logger = logging.getLogger('opentrons') stack_logger.propagate = propagate_logs contents = protocol_file.read() if custom_labware_paths: extra_labware = labware_from_paths(custom_labware_paths) else: extra_labware = {} if custom_data_paths: extra_data = datafiles_from_paths(custom_data_paths) else: extra_data = {} hardware_simulator = None if hardware_simulator_file_path: hardware_simulator = asyncio.get_event_loop().run_until_complete( load_simulator(pathlib.Path(hardware_simulator_file_path))) protocol = parse.parse(contents, file_name, extra_labware=extra_labware, extra_data=extra_data) bundle_contents: Optional[BundleContents] = None if getattr(protocol, 'api_level', APIVersion(2, 0)) < APIVersion(2, 0): def _simulate_v1(): opentrons.robot.disconnect() opentrons.robot.reset() scraper = CommandScraper(stack_logger, log_level, opentrons.robot.broker) exec(protocol.contents, {}) # type: ignore return scraper scraper = _simulate_v1() else: # we want a None literal rather than empty dict so get_protocol_api # will look for custom labware if this is a robot gpa_extras = getattr(protocol, 'extra_labware', None) or None context = get_protocol_api( getattr(protocol, 'api_level', MAX_SUPPORTED_VERSION), bundled_labware=getattr(protocol, 'bundled_labware', None), bundled_data=getattr(protocol, 'bundled_data', None), hardware_simulator=hardware_simulator, extra_labware=gpa_extras) scraper = CommandScraper(stack_logger, log_level, context.broker) try: execute.run_protocol(protocol, context) if isinstance(protocol, PythonProtocol)\ and protocol.api_level >= APIVersion(2, 0)\ and protocol.bundled_labware is None\ and allow_bundle(): bundle_contents = bundle_from_sim(protocol, context) finally: context.cleanup() return scraper.commands, bundle_contents
def test_parse_bundle_no_root_files(get_bundle_fixture, ensure_api2): fixture = get_bundle_fixture('no_root_files_bundle') filename = fixture['filename'] with pytest.raises(RuntimeError, match='No files found in ZIP file\'s root directory'): parse(fixture['binary_zipfile'], filename)
def test_execute_v1_imports(protocol, ensure_api2): proto = parse(protocol) execute.run_protocol(proto)
def test_execute_ok(protocol, protocol_file, ensure_api2, loop): proto = parse(protocol.text, protocol.filename) ctx = ProtocolContext(loop) execute.run_protocol(proto, context=ctx)
def test_legacy_jsonprotocol_v1(get_json_protocol_fixture): robot.reset() protocol_data = get_json_protocol_fixture('1', 'simple', False) protocol = parse(protocol_data, None) execute_protocol(protocol)
def test_parse_bundle_conflicting_labware(get_bundle_fixture, ensure_api2): fixture = get_bundle_fixture('conflicting_labware_bundle') filename = fixture['filename'] with pytest.raises(RuntimeError, match='Conflicting labware in bundle'): parse(fixture['binary_zipfile'], filename)
def test_parse_bundle_no_entrypoint_protocol(get_bundle_fixture, ensure_api2): fixture = get_bundle_fixture('no_entrypoint_protocol_bundle') filename = fixture['filename'] with pytest.raises(RuntimeError, match='Bundled protocol should have a'): parse(fixture['binary_zipfile'], filename)
def execute(protocol_file: TextIO, protocol_name: str, propagate_logs: bool = False, log_level: str = 'warning', emit_runlog: Callable[[Dict[str, Any]], None] = None, custom_labware_paths: List[str] = None, custom_data_paths: List[str] = None): """ Run the protocol itself. This is a one-stop function to run a protocol, whether python or json, no matter the api verson, from external (i.e. not bound up in other internal server infrastructure) sources. To run an opentrons protocol from other places, pass in a file like object as protocol_file; this function either returns (if the run has no problems) or raises an exception. To call from the command line use either the autogenerated entrypoint ``opentrons_execute`` or ``python -m opentrons.execute``. If the protocol is using Opentrons Protocol API V1, it does not need to explicitly call :py:meth:`.Robot.connect` or :py:meth:`.Robot.discover_modules`, or :py:meth:`.Robot.cache_instrument_models`. :param file-like protocol_file: The protocol file to execute :param str protocol_name: The name of the protocol file. This is required internally, but it may not be a thing we can get from the protocol_file argument. :param propagate_logs: Whether this function should allow logs from the Opentrons stack to propagate up to the root handler. This can be useful if you're integrating this function in a larger application, but most logs that occur during protocol simulation are best associated with the actions in the protocol that cause them. Default: ``False`` :type propagate_logs: bool :param log_level: The level of logs to emit on the command line.. Default: 'warning' :type log_level: 'debug', 'info', 'warning', or 'error' :param emit_runlog: A callback for printing the runlog. If specified, this will be called whenever a command adds an entry to the runlog, which can be used for display and progress estimation. If specified, the callback should take a single argument (the name doesn't matter) which will be a dictionary (see below). Default: ``None`` :param custom_labware_paths: A list of directories to search for custom labware, or None. Ignored if the apiv2 feature flag is not set. Loads valid labware from these paths and makes them available to the protocol context. :param custom_data_paths: A list of directories or files to load custom data files from. Ignored if the apiv2 feature flag if not set. Entries may be either files or directories. Specified files and the non-recursive contents of specified directories are presented by the protocol context in :py:attr:`.ProtocolContext.bundled_data`. The format of the runlog entries is as follows: .. code-block:: python { 'name': command_name, 'payload': { 'text': string_command_text, # The rest of this struct is command-dependent; see # opentrons.commands.commands. Its keys match format # keys in 'text', so that # entry['payload']['text'].format(**entry['payload']) # will produce a string with information filled in } } """ stack_logger = logging.getLogger('opentrons') stack_logger.propagate = propagate_logs stack_logger.setLevel(getattr(logging, log_level.upper(), logging.WARNING)) contents = protocol_file.read() if custom_labware_paths: extra_labware = labware_from_paths(custom_labware_paths) else: extra_labware = {} if custom_data_paths: extra_data = datafiles_from_paths(custom_data_paths) else: extra_data = {} protocol = parse(contents, protocol_name, extra_labware=extra_labware, extra_data=extra_data) if getattr(protocol, 'api_level', APIVersion(2, 0)) < APIVersion(2, 0): opentrons.robot.connect() opentrons.robot.cache_instrument_models() opentrons.robot.discover_modules() opentrons.robot.home() if emit_runlog: opentrons.robot.broker.subscribe(commands.command_types.COMMAND, emit_runlog) assert isinstance(protocol, PythonProtocol),\ 'Internal error: Only Python protocols may be executed in v1' exec(protocol.contents, {}) else: bundled_data = getattr(protocol, 'bundled_data', {}) bundled_data.update(extra_data) gpa_extras = getattr(protocol, 'extra_labware', None) or None context = get_protocol_api(getattr(protocol, 'api_level', MAX_SUPPORTED_VERSION), bundled_labware=getattr( protocol, 'bundled_labware', None), bundled_data=bundled_data, extra_labware=gpa_extras) if emit_runlog: context.broker.subscribe(commands.command_types.COMMAND, emit_runlog) context.home() try: execute_apiv2.run_protocol(protocol, context) finally: context.cleanup()
def simulate( protocol_file: TextIO, file_name: str, custom_labware_paths=None, custom_data_paths=None, propagate_logs=False, log_level='warning' ) -> Tuple[List[Mapping[str, Any]], Optional[BundleContents]]: """ Simulate the protocol itself. This is a one-stop function to simulate a protocol, whether python or json, no matter the api version, from external (i.e. not bound up in other internal server infrastructure) sources. To simulate an opentrons protocol from other places, pass in a file like object as protocol_file; this function either returns (if the simulation has no problems) or raises an exception. To call from the command line use either the autogenerated entrypoint ``opentrons_simulate`` (``opentrons_simulate.exe``, on windows) or ``python -m opentrons.simulate``. The return value is the run log, a list of dicts that represent the commands executed by the robot; and either the contents of the protocol that would be required to bundle, or ``None``. Each dict element in the run log has the following keys: - ``level``: The depth at which this command is nested - if this an aspirate inside a mix inside a transfer, for instance, it would be 3. - ``payload``: The command, its arguments, and how to format its text. For more specific details see :py:mod:`opentrons.commands`. To format a message from a payload do ``payload['text'].format(**payload)``. - ``logs``: Any log messages that occurred during execution of this command, as a logging.LogRecord :param file-like protocol_file: The protocol file to simulate. :param str file_name: The name of the file :param custom_labware_paths: A list of directories to search for custom labware, or None. Ignored if the apiv2 feature flag is not set. Loads valid labware from these paths and makes them available to the protocol context. :param custom_data_paths: A list of directories or files to load custom data files from. Ignored if the apiv2 feature flag if not set. Entries may be either files or directories. Specified files and the non-recursive contents of specified directories are presented by the protocol context in :py:attr:`.ProtocolContext.bundled_data`. :param propagate_logs: Whether this function should allow logs from the Opentrons stack to propagate up to the root handler. This can be useful if you're integrating this function in a larger application, but most logs that occur during protocol simulation are best associated with the actions in the protocol that cause them. Default: ``False`` :type propagate_logs: bool :param log_level: The level of logs to capture in the runlog. Default: ``'warning'`` :type log_level: 'debug', 'info', 'warning', or 'error' :returns: A tuple of a run log for user output, and possibly the required data to write to a bundle to bundle this protocol. The bundle is only emitted if the API v2 feature flag is set and this is an unbundled python protocol. In other cases it is None. """ stack_logger = logging.getLogger('opentrons') stack_logger.propagate = propagate_logs contents = protocol_file.read() if custom_labware_paths: extra_labware = labware_from_paths(custom_labware_paths) else: extra_labware = {} if custom_data_paths: extra_data = datafiles_from_paths(custom_data_paths) else: extra_data = {} protocol = parse.parse(contents, file_name, extra_labware=extra_labware, extra_data=extra_data) if isinstance(protocol, JsonProtocol)\ or protocol.api_level == '2'\ or (ff.enable_back_compat() and ff.use_protocol_api_v2()): context = get_protocol_api(protocol) scraper = CommandScraper(stack_logger, log_level, context.broker) execute.run_protocol(protocol, simulate=True, context=context) if isinstance(protocol, PythonProtocol)\ and protocol.bundled_labware is None: bundle_contents: Optional[BundleContents] = bundle_from_sim( protocol, context) else: bundle_contents = None else: def _simulate_v1(): import opentrons.legacy_api.protocols opentrons.robot.disconnect() scraper = CommandScraper(stack_logger, log_level, opentrons.robot.broker) if isinstance(protocol, JsonProtocol): opentrons.legacy_api.protocols.execute_protocol(protocol) else: exec(protocol.contents, {}) return scraper scraper = _simulate_v1() bundle_contents = None return scraper.commands, bundle_contents