예제 #1
0
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)
예제 #2
0
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)
예제 #3
0
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)
예제 #4
0
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')
예제 #5
0
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)
예제 #6
0
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)
예제 #7
0
 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
예제 #8
0
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
예제 #9
0
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)
예제 #10
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])
예제 #11
0
def test_bad_structure(bad_protocol):
    with pytest.raises(MalformedProtocolError):
        parse(bad_protocol)
예제 #12
0
 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
예제 #13
0
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
예제 #14
0
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, {})
예제 #15
0
def test_get_version(proto, version):
    parsed = parse(proto)
    assert parsed.api_level == version
예제 #16
0
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
예제 #17
0
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)
예제 #18
0
def test_execute_v1_imports(protocol, ensure_api2):
    proto = parse(protocol)
    execute.run_protocol(proto)
예제 #19
0
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)
예제 #20
0
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)
예제 #21
0
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)
예제 #22
0
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)
예제 #23
0
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()
예제 #24
0
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