pytest_robotframework

pytest-robotframework

pytest-robotframework is a pytest plugin that creates robotframework reports for tests written in python and allows you to run robotframework tests with pytest.

image

install

Stable Version Conda Version

pytest should automatically find and activate the plugin once you install it.

API documentation

features

write robot tests in python

# you can use both robot and pytest features
from robot.api import logger
from pytest import Cache

from pytest_robotframework import keyword

@keyword  # make this function show as a keyword in the robot log
def foo():
    ...

@mark.slow  # markers get converted to robot tags
def test_foo():
    foo()

run .robot tests

to allow for gradual adoption, the plugin also runs regular robot tests as well:

*** Settings ***
test setup  foo

*** Test Cases ***
bar
    [Tags]  asdf  key:value
    no operation

*** Keywords ***
foo
    log  ran setup

which is roughly equivalent to the following python code:

# test_foo.py
from pytest import mark

@keyword
def foo():
    logger.info("ran setup")

@fixture(autouse=True)
def setup():
    foo()

@mark.asdf
@mark.key("value")
def test_bar():
    ...

setup/teardown

in pytest, setups and teardowns are defined using fixtures:

from pytest import fixture
from robot.api import logger

@fixture
def user():
    logger.info("logging in")
    user = ...
    yield user
    logger.info("logging off")

def test_something(user):
    ...

under the hood, pytest calls the fixture setup/teardown code as part of the pytest_runtest_setup and and pytest_runtest_teardown hooks, which appear in the robot log like so:

image

for more information, see the pytest documentation for fixtures and hook functions.

tags/markers

pytest markers are converted to tags in the robot log:

from pytest import mark

@mark.slow
def test_blazingly_fast_sorting_algorithm():
    [1,2,3].sort()

markers like skip, skipif and parameterize also work how you'd expect:

from pytest import mark

@mark.parametrize("test_input,expected", [(1, 8), (6, 6)])
def test_eval(test_input: int, expected: int):
    assert test_input == expected

image

robot suite variables

to set suite-level robot variables, call the set_variables function at the top of the test suite:

from robot.libraries.BuiltIn import BuiltIn
from pytest_robotframework import set_variables

set_variables(
    {
        "foo": "bar",
        "baz": ["a", "b"],
    }
)

def test_variables():
    assert BuiltIn().get_variable_value("$foo") == "bar"

set_variables is equivalent to the *** Variables *** section in a .robot file. all variables are prefixed with $. @ and & are not required since $ variables can store lists and dicts anyway

running tests in parallel

running tests in parallel using pytest-xdist is supported. when running with xdist, pytest-robotframework will run separate instances of robot for each test, then merge the robot output files together automatically using rebot.

config

pass --capture=no to make logger.console work properly.

since this is a pytest plugin, you should avoid using robot options that have pytest equivalents:

instead of... use... notes
robot --include tag_name pytest -m tag_name
robot --exclude tag_name pytest -m not tag_name
robot --skip tag_name pytest -m "not tag_name"
robot --test "test name" ./test.robot pytest ./test.robot::"Test Name"
robot --suite "suite name" ./folder pytest ./folder
robot --dryrun pytest --collect-only not exactly the same. you should use a type checker on your python tests as a replacement for robot dryrun
robot --exitonfailure pytest --maxfail=1
robot --rerunfailed pytest --lf
robot --runemptysuite pytest --suppress-no-test-exit-code requires the pytest-custom-exit-code plugin
robot --help pytest --help all supported robot options will be listed in the robotframework section

specifying robot options directlty

there are multiple ways you can specify the robot arguments directly. however, arguments that have pytest equivalents cannot be set with robot as they would cause the plugin to behave incorrectly.

pytest cli arguments

most robot cli arguments can be passed to pytest by prefixing the argument names with --robot-. for example, here's how to change the log level:

before

robot --loglevel DEBUG:INFO foo.robot

after

pytest --robot-loglevel DEBUG:INFO test_foo.py

you can see a complete list of the available arguments using the pytest --help command. any robot arguments not present in that list are not supported because they are replaced by a pytest equivalent (see above).

pytest_robot_modify_options hook

you can specify a pytest_robot_modify_options hook in your conftest.py to programmatically modify the arguments. see the pytest_robotframework.hooks documentation for more information.

from pytest_robotframework import RobotOptions
from robot.api.interfaces import ListenerV3

class Foo(ListenerV3):
    ...

def pytest_robot_modify_options(options: RobotOptions, session: Session) -> None:
    if not session.config.option.collectonly:
        options["loglevel"] = "DEBUG:INFO"
        options["listener"].append(Foo()) # you can specify instances as listeners, prerebotmodifiers, etc.

note that not all arguments that the plugin passes to robot will be present in the args list. arguments required for the plugin to function (eg. the plugin's listeners and prerunmodifiers) cannot be viewed or modified with this hook

ROBOT_OPTIONS environment variable

ROBOT_OPTIONS="-d results --listener foo.Foo"

enabling pytest assertions in the robot log

by default, only failed assertions will appear in the log. to make passed assertions show up, you'll have to add enable_assertion_pass_hook = true to your pytest ini options:

# pyproject.toml
[tool.pytest.ini_options]
enable_assertion_pass_hook = true

image

hiding non-user facing assertions

you may have existing assert statements in your codebase that are not intended to be part of your tests (eg. for narrowing types/validating input data) and don't want them to show up in the robot log. there are two ways you can can hide individual assert statements from the log:

from pytest_robotframework import AssertOptions, hide_asserts_from_robot_log

def test_foo():
    # hide a single passing `assert` statement:
    assert foo == bar, AssertOptions(log_pass=False)

    # hide a group of passing `assert` statements:
    with hide_asserts_from_robot_log():
        assert foo == bar
        assert bar == baz

note that failing assert statements will still show in the log regardless.

you can also run pytest with the --no-assertions-in-robot-log argument to disable assert statements in the robot log by default, then use AssertOptions to explicitly enable individual assert statements:

from pytest_robotframework import AssertOptions

def test_foo():
    assert "foo" == "bar" # hidden from the robot log (when run with --no-assertions-in-robot-log)
    assert "bar" == "baz", AssertOptions(log_pass=True) # not hidden

customizing assertions

pytest-robotframework allows you to customize the message for the assert keyword which appears on both passing and failing assertions:

assert 1 == 1  # no custom description
assert 1 == 1, AssertOptions(description="custom description")

image

you can still pass a custom message to be displayed only when your assertion fails:

assert 1 == 2, "the values did not match"

however if you want to specify both a custom description and a failure message, you can use the fail_message argument:

assert 1 == 2, "failure message"
assert 1 == 2, AssertOptions(description="checking values", fail_message="failure message")

image

note that enable_assertion_pass_hook pytest option needs to be enabled for this to work.

limitations with tests written in python

there are some limitations when writing robotframework tests in python. pytest-robotframework includes solutions for these issues.

making keywords show in the robot log

by default when writing tests in python, the only keywords that you'll see in the robot log are Setup, Run Test and Teardown. this is because robot is not capable of recognizing keywords called outside of robot code. (see this issue)

this plugin has several workarounds for the problem:

@keyword decorator

if you want a function you wrote to show up as a keyword in the log, decorate it with the pytest_robotframework.keyword instead of robot.api.deco.keyword

from pytest_robotframework import keyword

@keyword
def foo():
    ...

pytest functions are patched by the plugin

most of the pytest functions are patched so that they show as keywords in the robot log

def test_foo():
    with pytest.raises(ZeroDivisionError):
        logger.info(1 / 0)

image

patching third party functions with keywordify

if you want a function from a third party module/robot library to be displayed as a keyword, you can patch it with the keywordify function:

# in your conftest.py

from pyest_robotframework import keywordify
import some_module

# patch a function from the module:
keywordify(some_module, "some_function")
# works on classes too:
keywordify(some_module.SomeClass, "some_method")

continuable failures don't work

keywords that raise ContinuableFailure don't work properly when called from python code. this includes builtin keywords such as Run Keyword And Continue On Failure.

use pytest.raises for expected failures instead:

from pytest import raises

with raises(SomeException):
    some_keyword_that_fails()

or if the exception is conditionally raised, use a try/except statement like you would in regular python code:

try:
    some_keyword_that_fails()
except SomeException:
    ... # ignore the exception, or re-raise it later

the keyword will still show as failed in the log (as long as it's decorated with pytest_robotframework.keyword), but it won't effect the status of the test unless the exception is re-raised.

why?

robotframework introduced TRY/EXCEPT statements in version 5.0, which they now recommend using instead of the old Run Keyword And Ignore Error/Run Keyword And Expect Error keywords.

however TRY/EXCEPT behaves differently to its python equivalent, as it allows for errors that do not actually raise an exception to be caught:

*** Test Cases ***
Foo
    TRY
        Run Keyword And Continue On Failure    Fail
        Log    this is executed
    EXCEPT
        Log    and so is this
    END

this means that if control flows like Run Keyword And Continue On Failure were supported, its failures would be impossible to catch:

from robot.api.logger import info
from robot.libraries.BuiltIn import BuiltIn

try:
    BuiltIn().run_keyword_and_continue_on_failure("fail")
    info("this is executed because an exception was not actually raised")
except:
    info("this is NOT executed, but the test will still fail")

IDE integration

vscode

vscode's builtin python plugin should discover both your python and robot tests by default, and show run buttons next to them:

image image

running .robot tests

if you still intend to use .robot files with pytest-robotframework, we recommend using the robotcode extension and setting robotcode.testExplorer.enabled to false in .vscode/settings.json. this will prevent the tests from being duplicated in the test explorer.

pycharm

pycharm currently does not support pytest plugins for non-python files. see this issue

compatibility

dependency version range comments
python >=3.9,<4.0 all versions of python will be supported until their end-of-life as described here
robotframework >=6.1,<8.0 i will try to support at least the two most recent major versions. robot 6.0 is not supported as the parser API that the plugin relies on to support tests written in python was introduced in version 6.1
pytest >=7.0,<9.0 may work on other versions, but things may break since this plugin relies on some internal pytest modules

API

useful helpers for you to use in your pytest tests and conftest.py files

  1"""
  2.. include:: ../README.md
  3
  4# API
  5useful helpers for you to use in your pytest tests and `conftest.py` files
  6"""
  7
  8from __future__ import annotations
  9
 10import inspect
 11from collections import defaultdict
 12from contextlib import AbstractContextManager, contextmanager, nullcontext
 13from functools import wraps
 14from pathlib import Path
 15from traceback import format_stack
 16from types import TracebackType
 17from typing import TYPE_CHECKING, Callable, TypeVar, Union, cast, final, overload
 18
 19from basedtyping import Function, P, T
 20from pytest import StashKey
 21from robot import result, running
 22from robot.api import deco, logger
 23from robot.errors import DataError, ExecutionFailed
 24from robot.libraries.BuiltIn import BuiltIn
 25from robot.model.visitor import SuiteVisitor
 26from robot.running import model
 27from robot.running.context import _ExecutionContext  # pyright:ignore[reportPrivateUsage]
 28from robot.running.librarykeywordrunner import LibraryKeywordRunner
 29from robot.running.statusreporter import ExecutionStatus, HandlerExecutionFailed, StatusReporter
 30from robot.utils import getshortdoc, printable_name
 31from robot.utils.error import ErrorDetails
 32from typing_extensions import Literal, Never, TypeAlias, deprecated, override
 33
 34from pytest_robotframework._internal.cringe_globals import current_item, current_session
 35from pytest_robotframework._internal.errors import InternalError
 36from pytest_robotframework._internal.robot.utils import (
 37    Listener as _Listener,
 38    RobotOptions as _RobotOptions,
 39    add_robot_error,
 40    escape_robot_str,
 41    execution_context,
 42    get_arg_with_type,
 43    is_robot_traceback,
 44    robot_6,
 45)
 46
 47if TYPE_CHECKING:
 48    from collections.abc import Iterable, Iterator, Mapping
 49
 50
 51RobotVariables: TypeAlias = dict[str, object]
 52"""variable names and values to be set on the suite level. see the `set_variables` function"""
 53
 54_suite_variables = defaultdict[Path, RobotVariables](dict)
 55
 56
 57def set_variables(variables: RobotVariables) -> None:
 58    """
 59    sets suite-level variables, equivalent to the `*** Variables ***` section in a `.robot` file.
 60
 61    also performs some validation checks that robot doesn't to make sure the variable has the
 62    correct type matching its prefix.
 63    """
 64    suite_path = Path(inspect.stack()[1].filename)
 65    _suite_variables[suite_path] = variables
 66
 67
 68_resources: list[Path] = []
 69
 70
 71def import_resource(path: Path | str) -> None:
 72    """
 73    imports the specified robot `.resource` file when the suite execution begins.
 74    use this when specifying robot resource imports at the top of the file.
 75
 76    to import libraries, use a regular python import
 77    """
 78    if execution_context():
 79        BuiltIn().import_resource(escape_robot_str(str(path)))
 80    else:
 81        _resources.append(Path(path))
 82
 83
 84class _FullStackStatusReporter(StatusReporter):
 85    """
 86    Riced status reporter that does the following:
 87
 88    - inserts the full test traceback into exceptions raisec within it (otherwise it would only go
 89    back to the start of the keyword, instead of the whole test)
 90    - does not log failures when they came from a nested keyword, to prevent errors from being
 91    duplicated for each keyword in the stack
 92    """
 93
 94    @override
 95    def _get_failure(self, *args: Never, **kwargs: Never):
 96        exc_value = get_arg_with_type(BaseException, args, kwargs)
 97        context = get_arg_with_type(_ExecutionContext, args, kwargs)
 98        if not context:
 99            raise Exception(
100                f"failed to find execution context in {_FullStackStatusReporter.__name__}"
101            )
102        if exc_value is None:
103            return None
104        if isinstance(exc_value, ExecutionStatus):
105            return exc_value
106        if isinstance(exc_value, DataError):
107            msg = exc_value.message
108            context.fail(msg)
109            return ExecutionFailed(msg, syntax=exc_value.syntax)
110
111        tb = None
112        full_system_traceback = inspect.stack()
113        in_framework = True
114        base_tb = exc_value.__traceback__
115        while base_tb and is_robot_traceback(base_tb):
116            base_tb = base_tb.tb_next
117        for frame in full_system_traceback:
118            trace = TracebackType(
119                tb or base_tb, frame.frame, frame.frame.f_lasti, frame.frame.f_lineno
120            )
121            if in_framework and is_robot_traceback(trace):
122                continue
123            in_framework = False
124            tb = trace
125            # find a frame from a module that should always be in the trace
126            if Path(frame.filename) == Path(model.__file__):
127                break
128        else:
129            # using logger.error because raising an exception here would screw up the output xml
130            logger.error(
131                str(
132                    InternalError(
133                        "failed to filter out pytest-robotframework machinery for exception: "
134                        f"{exc_value!r}\n\nfull traceback:\n\n"
135                        "".join(format_stack())
136                    )
137                )
138            )
139        exc_value.__traceback__ = tb
140
141        error = ErrorDetails(exc_value)
142        failure = HandlerExecutionFailed(error)
143        if failure.timeout:
144            context.timeout_occurred = True
145        # if there is more than 1 wrapped error, that means it came from a child keyword and
146        # therefore has already been logged by its status reporter
147        is_nested_status_reporter_failure = len(_get_status_reporter_failures(exc_value)) > 1
148        if failure.skip:
149            context.skip(error.message)
150        elif not is_nested_status_reporter_failure:
151            context.fail(error.message)
152        if not is_nested_status_reporter_failure and error.traceback:
153            context.debug(error.traceback)
154        return failure
155
156
157_status_reporter_exception_attr = "__pytest_robot_status_reporter_exceptions__"
158
159
160def _get_status_reporter_failures(exception: BaseException) -> list[HandlerExecutionFailed]:
161    """
162    normally, robot wraps exceptions from keywords in a `HandlerExecutionFailed` or
163    something, but we want to preserve the original exception so that users can use
164    `try`/`except` without having to worry about their expected exception being wrapped in
165    something else, so instead we just add this attribute to the existing exception so we can
166    refer to it after the test is over, to determine if we still need to log the failure or if
167    it was already logged inside a keyword
168
169    it's a stack because we need to check if there is more than 1 wrapped exception in
170    `FullStackStatusReporter`
171    """
172    wrapped_error: list[HandlerExecutionFailed] | None = getattr(
173        exception, _status_reporter_exception_attr, None
174    )
175    if wrapped_error is None:
176        wrapped_error = []
177        setattr(exception, _status_reporter_exception_attr, wrapped_error)
178    return wrapped_error
179
180
181_keyword_original_function_attr = "__pytest_robot_keyword_original_function__"
182
183
184class _KeywordDecorator:
185    def __init__(
186        self,
187        *,
188        name: str | None = None,
189        tags: tuple[str, ...] | None = None,
190        module: str | None = None,
191        doc: str | None = None,
192    ) -> None:
193        super().__init__()
194        self._name: str | None = name
195        self._tags: tuple[str, ...] = tags or ()
196        self._module: str | None = module
197        self._doc: str | None = doc
198
199    @staticmethod
200    def _save_status_reporter_failure(exception: BaseException):
201        stack = _get_status_reporter_failures(exception)
202        stack.append(HandlerExecutionFailed(ErrorDetails(exception)))
203
204    @classmethod
205    def inner(
206        cls,
207        fn: Callable[P, T],
208        status_reporter: AbstractContextManager[object, bool],
209        /,
210        *args: P.args,
211        **kwargs: P.kwargs,
212    ) -> T:
213        error: BaseException | None = None
214        with status_reporter:
215            try:
216                result_ = fn(*args, **kwargs)
217            except BaseException as e:
218                cls._save_status_reporter_failure(e)
219                error = e
220                raise
221        if error:
222            raise error
223        # pyright assumes the assignment to error could raise an exception but that will NEVER
224        # happen
225        return result_  # pyright:ignore[reportReturnType,reportPossiblyUnboundVariable]
226
227    def call(self, fn: Callable[P, T]) -> Callable[P, T]:
228        if isinstance(fn, _KeywordDecorator):
229            return fn
230        keyword_name = self._name or cast(str, printable_name(fn.__name__, code_style=True))
231        # this doesn't really do anything in python land but we call the original robot keyword
232        # decorator for completeness
233        deco.keyword(  # pyright:ignore[reportUnknownMemberType]
234            name=keyword_name, tags=self._tags
235        )(fn)
236
237        @wraps(fn)
238        def inner(*args: P.args, **kwargs: P.kwargs) -> T:
239            def truncate(arg: object) -> str:
240                """
241                robotframework usually just uses the argument as it was written in the source
242                code, but since we can't easily access that in python, we use the actual value
243                instead, but that can sometimes be huge so we truncate it. you can see the full
244                value when running with the DEBUG loglevel anyway
245                """
246                max_length = 50
247                value = str(arg)
248                return value[:max_length] + "..." if len(value) > max_length else value
249
250            if self._module is None:
251                self._module = fn.__module__
252            log_args = (
253                *(truncate(arg) for arg in args),
254                *(f"{key}={truncate(value)}" for key, value in kwargs.items()),
255            )
256            context = execution_context()
257            data = running.Keyword(name=keyword_name, args=log_args)
258            doc: str = (getshortdoc(inspect.getdoc(fn)) or "") if self._doc is None else self._doc
259            # we suppress the error in the status reporter because we raise it ourselves
260            # afterwards, so that context managers like `pytest.raises` can see the actual
261            # exception instead of `robot.errors.HandlerExecutionFailed`
262            suppress = True
263            # nullcontext is typed as returning None which pyright incorrectly marks as
264            # unreachable. see https://github.com/DetachHead/basedpyright/issues/10
265            context_manager: AbstractContextManager[object, bool] = (  # pyright:ignore[reportAssignmentType]
266                (
267                    _FullStackStatusReporter(
268                        data=data,
269                        result=(
270                            result.Keyword(
271                                # pyright is only run when robot 7 is installed
272                                kwname=keyword_name,  # pyright:ignore[reportCallIssue]
273                                libname=self._module,  # pyright:ignore[reportCallIssue]
274                                doc=doc,
275                                args=log_args,
276                                tags=self._tags,
277                            )
278                        ),
279                        context=context,
280                        suppress=suppress,
281                    )
282                    if robot_6
283                    else (
284                        _FullStackStatusReporter(
285                            data=data,
286                            result=result.Keyword(
287                                name=keyword_name,
288                                owner=self._module,
289                                doc=doc,
290                                args=log_args,
291                                tags=self._tags,
292                            ),
293                            context=context,
294                            suppress=suppress,
295                            implementation=cast(
296                                LibraryKeywordRunner, context.get_runner(keyword_name)
297                            ).keyword.bind(data),
298                        )
299                    )
300                )
301                if context
302                else nullcontext()
303            )
304            return self.inner(fn, context_manager, *args, **kwargs)
305
306        setattr(inner, _keyword_original_function_attr, fn)
307        return inner
308
309
310class _FunctionKeywordDecorator(_KeywordDecorator):
311    """
312    decorator for a keyword that does not return a context manager. does not allow functions that
313    return context managers. if you want to decorate a context manager, pass the
314    `wrap_context_manager` argument to the `keyword` decorator
315    """
316
317    @deprecated(
318        "you must explicitly pass `wrap_context_manager` when using `keyword` with a"
319        " context manager"
320    )
321    @overload
322    def __call__(self, fn: Callable[P, AbstractContextManager[T]]) -> Never: ...
323
324    @overload
325    def __call__(self, fn: Callable[P, T]) -> Callable[P, T]: ...
326
327    def __call__(self, fn: Callable[P, T]) -> Callable[P, T]:
328        return self.call(fn)
329
330
331_T_ContextManager = TypeVar("_T_ContextManager", bound=AbstractContextManager[object])
332
333
334class _NonWrappedContextManagerKeywordDecorator(_KeywordDecorator):
335    """
336    decorator for a function that returns a context manager. only wraps the function as a keyword
337    but not the body of the context manager it returns. to do that, pass `wrap_context_manager=True`
338    """
339
340    def __call__(self, fn: Callable[P, _T_ContextManager]) -> Callable[P, _T_ContextManager]:
341        return self.call(fn)
342
343
344class _WrappedContextManagerKeywordDecorator(_KeywordDecorator):
345    """
346    decorator for a function that returns a context manager. only wraps the body of the context
347    manager it returns
348    """
349
350    @classmethod
351    @override
352    def inner(
353        cls,
354        fn: Callable[P, T],
355        status_reporter: AbstractContextManager[object],
356        /,
357        *args: P.args,
358        **kwargs: P.kwargs,
359    ) -> T:
360        T_WrappedContextManager = TypeVar("T_WrappedContextManager")
361
362        @final
363        class WrappedContextManager(AbstractContextManager[object]):
364            """
365            defers exiting the status reporter until after the wrapped context
366            manager is finished
367            """
368
369            def __init__(
370                self,
371                wrapped: AbstractContextManager[T_WrappedContextManager],
372                status_reporter: AbstractContextManager[object],
373            ) -> None:
374                super().__init__()
375                self.wrapped = wrapped
376                self.status_reporter = status_reporter
377
378            @override
379            # https://github.com/DetachHead/basedpyright/issues/294
380            def __enter__(self) -> object:  # pyright:ignore[reportMissingSuperCall]
381                _ = self.status_reporter.__enter__()
382                return self.wrapped.__enter__()
383
384            @override
385            def __exit__(
386                self,
387                exc_type: type[BaseException] | None,
388                exc_value: BaseException | None,
389                traceback: TracebackType | None,
390                /,
391            ) -> bool:
392                suppress = False
393                try:
394                    suppress = self.wrapped.__exit__(exc_type, exc_value, traceback)
395                except BaseException as e:
396                    e.__context__ = exc_value
397                    exc_value = e
398                    raise
399                finally:
400                    error = None if suppress else exc_value
401                    if error is None:
402                        _ = self.status_reporter.__exit__(None, None, None)
403                    else:
404                        cls._save_status_reporter_failure(error)  # pyright:ignore[reportPrivateUsage]
405                        _ = self.status_reporter.__exit__(type(error), error, error.__traceback__)
406                return suppress or False
407
408        fn_result = fn(*args, **kwargs)
409        if not isinstance(fn_result, AbstractContextManager):
410            raise TypeError(
411                f"keyword decorator expected a context manager but instead got {fn_result!r}"
412            )
413        # 🚀 independently verified for safety by the overloads
414        return WrappedContextManager(  # pyright:ignore[reportReturnType]
415            fn_result, status_reporter
416        )
417
418    def __call__(
419        self, fn: Callable[P, AbstractContextManager[T]]
420    ) -> Callable[P, AbstractContextManager[T]]:
421        return self.call(fn)
422
423
424@overload
425def keyword(
426    *,
427    name: str | None = ...,
428    tags: tuple[str, ...] | None = ...,
429    module: str | None = ...,
430    wrap_context_manager: Literal[True],
431) -> _WrappedContextManagerKeywordDecorator: ...
432
433
434@overload
435def keyword(
436    *,
437    name: str | None = ...,
438    tags: tuple[str, ...] | None = ...,
439    module: str | None = ...,
440    wrap_context_manager: Literal[False],
441) -> _NonWrappedContextManagerKeywordDecorator: ...
442
443
444@overload
445def keyword(
446    *,
447    name: str | None = ...,
448    tags: tuple[str, ...] | None = ...,
449    module: str | None = ...,
450    wrap_context_manager: None = ...,
451) -> _FunctionKeywordDecorator: ...
452
453
454@overload
455# prevent functions that return Never from matching the context manager overload
456def keyword(fn: Callable[P, Never]) -> Callable[P, Never]: ...
457
458
459@deprecated(
460    "you must explicitly pass `wrap_context_manager` when using `keyword` with a context manager"
461)
462@overload
463def keyword(fn: Callable[P, AbstractContextManager[T]]) -> Never: ...
464
465
466@overload
467def keyword(fn: Callable[P, T]) -> Callable[P, T]: ...
468
469
470def keyword(  # pylint:disable=missing-param-doc
471    fn: Callable[P, T] | None = None,
472    *,
473    name: str | None = None,
474    tags: tuple[str, ...] | None = None,
475    module: str | None = None,
476    wrap_context_manager: bool | None = None,
477) -> _KeywordDecorator | Callable[P, T]:
478    """
479    marks a function as a keyword and makes it show in the robot log.
480
481    unlike robot's `deco.keyword` decorator, this one will make your function appear as a keyword in
482    the robot log even when ran from a python file.
483
484    if the function returns a context manager, its body is included in the keyword (just make sure
485    the `@keyword` decorator is above `@contextmanager`)
486
487    :param name: set a custom name for the keyword in the robot log (default is inferred from the
488    decorated function name). equivalent to `robot.api.deco.keyword`'s `name` argument
489    :param tags: equivalent to `robot.api.deco.keyword`'s `tags` argument
490    :param module: customize the module that appears top the left of the keyword name in the log.
491    defaults to the function's actual module
492    :param wrap_context_manager: if the decorated function returns a context manager, whether or not
493    to wrap the context manager instead of the function. you probably always want this to be `True`,
494    unless you don't always intend to use the returned context manager.
495    """
496    if fn is None:
497        if wrap_context_manager is None:
498            return _FunctionKeywordDecorator(name=name, tags=tags, module=module)
499        if wrap_context_manager:
500            return _WrappedContextManagerKeywordDecorator(name=name, tags=tags, module=module)
501        return _NonWrappedContextManagerKeywordDecorator(name=name, tags=tags, module=module)
502    return keyword(  # pyright:ignore[reportReturnType]
503        name=name, tags=tags, module=module, wrap_context_manager=wrap_context_manager
504    )(fn)  # pyright:ignore[reportArgumentType]
505
506
507def as_keyword(
508    name: str,
509    *,
510    doc: str = "",
511    tags: tuple[str, ...] | None = None,
512    args: Iterable[str] | None = None,
513    kwargs: Mapping[str, str] | None = None,
514) -> AbstractContextManager[None]:
515    """
516    runs the body as a robot keyword.
517
518    example:
519    -------
520    >>> with as_keyword("do thing"):
521    ...     ...
522
523    :param name: the name for the keyword
524    :param doc: the documentation to be displayed underneath the keyword in the robot log
525    :param tags: tags for the keyword
526    :param args: positional arguments to be displayed on the keyword in the robot log
527    :param kwargs: keyword arguments to be displayed on the keyword in the robot log
528    """
529
530    @_WrappedContextManagerKeywordDecorator(name=name, tags=tags, doc=doc, module="")
531    @contextmanager
532    def fn(*_args: str, **_kwargs: str) -> Iterator[None]:
533        yield
534
535    return fn(*(args or []), **(kwargs or {}))
536
537
538def keywordify(
539    obj: object,
540    method_name: str,
541    *,
542    name: str | None = None,
543    tags: tuple[str, ...] | None = None,
544    module: str | None = None,
545    wrap_context_manager: bool = False,
546) -> None:
547    """
548    patches a function to make it show as a keyword in the robot log.
549
550    you should only use this on third party modules that you don't control. if you want your own
551    function to show as a keyword you should decorate it with `@keyword` instead (the one from this
552    module, not the one from robot)
553
554    :param obj: the object with the method to patch on it (this has to be specified separately as
555    the object itself needs to be modified with the patched method)
556    :param method_name: the name of the method to patch
557    :param name: set a custom name for the keyword in the robot log (default is inferred from the
558    decorated function name). equivalent to `robot.api.deco.keyword`'s `name` argument
559    :param tags: equivalent to `robot.api.deco.keyword`'s `tags` argument
560    :param module: customize the module that appears top the left of the keyword name in the log.
561    defaults to the function's actual module
562    :param wrap_context_manager: if the decorated function returns a context manager, whether or not
563    to wrap the context manager instead of the function. you probably always want this to be `True`,
564    unless you don't always intend to use the returned context manager
565    """
566    setattr(
567        obj,
568        method_name,
569        keyword(name=name, tags=tags, module=module, wrap_context_manager=wrap_context_manager)(
570            getattr(obj, method_name)  # pyright:ignore[reportAny]
571        ),
572    )
573
574
575_T_ListenerOrSuiteVisitor = TypeVar(
576    "_T_ListenerOrSuiteVisitor", bound=type[Union["Listener", SuiteVisitor]]
577)
578
579
580def catch_errors(cls: _T_ListenerOrSuiteVisitor) -> _T_ListenerOrSuiteVisitor:
581    """
582    errors that occur inside suite visitors and listeners do not cause the test run to fail. even
583    `--exitonerror` doesn't catch every exception (see <https://github.com/robotframework/robotframework/issues/4853>).
584
585    this decorator will remember any errors that occurred inside listeners and suite visitors, then
586    raise them after robot has finished running.
587
588    you don't need this if you are using the `listener` or `pre_rebot_modifier` decorator, as
589    those decorators use `catch_errors` as well
590    """
591    # prevent classes from being wrapped twice
592    marker = "_catch_errors"
593    if hasattr(cls, marker):
594        return cls
595
596    def wrapped(fn: Callable[P, T]) -> Callable[P, T]:
597        @wraps(fn)
598        def inner(*args: P.args, **kwargs: P.kwargs) -> T:
599            try:
600                return fn(*args, **kwargs)
601            except Exception as e:
602                item_or_session = current_item() or current_session()
603                if not item_or_session:
604                    raise InternalError(
605                        # stack trace isn't showsn so we neewd to include the original error in the
606                        # message as well
607                        f"an error occurred inside {cls.__name__} and failed to get the"
608                        f" current pytest item/session: {e}"
609                    ) from e
610                add_robot_error(item_or_session, str(e))
611                raise
612
613        return inner
614
615    for name, method in cast(
616        list[tuple[str, Function]],
617        inspect.getmembers(
618            cls,
619            predicate=lambda attr: inspect.isfunction(attr)  # pyright:ignore[reportAny]
620            # the wrapper breaks static methods idk why, but we shouldn't need to wrap them anyway
621            # because robot listeners/suite visitors don't call any static/class methods
622            and not isinstance(
623                inspect.getattr_static(cls, attr.__name__), (staticmethod, classmethod)
624            )
625            # only wrap methods that are overwritten on the subclass
626            and attr.__name__ in vars(cls)
627            # don't wrap private/dunder methods since they'll get called by the public ones and we
628            # don't want to duplicate errors
629            and not attr.__name__.startswith("_"),
630        ),
631    ):
632        setattr(cls, name, wrapped(method))
633    setattr(cls, marker, True)
634    return cls
635
636
637class AssertOptions:
638    """
639    pass this as the second argument to an `assert` statement to customize how it appears in the
640    robot log.
641
642    example:
643    -------
644    .. code-block:: python
645
646        assert foo == bar, AssertOptions(
647            log_pass=False, description="checking the value", fail_msg="assertion failed"
648        )
649    """
650
651    def __init__(
652        self,
653        *,
654        log_pass: bool | None = None,
655        description: str | None = None,
656        fail_message: str | None = None,
657    ) -> None:
658        super().__init__()
659        self.log_pass: bool | None = log_pass
660        """whether to display the assertion as a keyword in the robot log when it passes.
661
662        by default, a passing `assert` statement will display in the robot log as long as the
663        following conditions are met:
664        - the `enable_assertion_pass_hook` pytest option is enabled
665        - it is not inside a `hide_asserts_from_robot_log` context manager
666        (see [enabling pytest assertions in the robot log](https://github.com/DetachHead/pytest-robotframework/#enabling-pytest-assertions-in-the-robot-log)).
667        - pytest is not run with the `--no-asserts-in-robot-log` argument
668
669        failing `assert` statements will show as keywords in the log as long as the
670        `enable_assertion_pass_hook` pytest option is enabled. if it's disabled, the assertion error
671        will be logged, but not within a keyword.
672
673        example:
674        -------
675        .. code-block:: python
676
677            # (assuming all of these assertions pass)
678
679            # never displays in the robot log:
680            assert foo == bar, AssertOptions(log_pass=False)
681
682            # always displays in the robot log (as long as the `enable_assertion_pass_hook` pytest
683            # option is enabled):
684            assert foo == bar, AssertOptions(log_pass=True)
685
686            # displays in the robot log as only if all 3 conditions mentioned above are met:
687            assert foo == bar
688        """
689
690        self.description: str | None = description
691        """normally, the asserted expression as it was written is displayed as the argument to the
692        `assert` keyword in the robot log, but setting this value will display a custom message
693        instead. when a custom description is used, the original expression is logged inside the
694        keyword instead."""
695
696        self.fail_message: str | None = fail_message
697        """optional description for the `assert` statement that will be included in the
698        `AssertionError` message if the assertion fails. equivalent to a normal `assert` statement's
699        second argument"""
700
701    @override
702    def __repr__(self) -> str:
703        """make the custom fail message appear in the call to `AssertionError`"""
704        return self.fail_message or ""
705
706
707_hide_asserts_context_manager_key = StashKey[bool]()
708
709
710@contextmanager
711def hide_asserts_from_robot_log() -> Iterator[None]:
712    """
713    context manager for hiding multiple passing `assert` statements from the robot log. note that
714    individual `assert` statements using `AssertOptions(log_pass=True)` take precedence, and that
715    failing assertions will always appear in the log.
716
717    when hiding only a single `assert` statement, you should use `AssertOptions(log=False)` instead.
718
719    example:
720    -------
721    .. code-block:: python
722
723        assert True  # not hidden
724        with hide_asserts_from_robot_log():
725            assert True  # hidden
726            assert True, AssertOptions(log_pass=True)  # not hidden
727    """
728    item = current_item()
729    if not item:
730        raise InternalError(
731            f"failed to get current pytest item in {hide_asserts_from_robot_log.__name__}"
732        )
733    previous_value = item.stash.get(_hide_asserts_context_manager_key, False)
734    item.stash[_hide_asserts_context_manager_key] = True
735    try:
736        yield
737    finally:
738        item.stash[_hide_asserts_context_manager_key] = previous_value
739
740
741# ideally these would just use an explicit re-export
742# https://github.com/mitmproxy/pdoc/issues/667
743Listener: TypeAlias = _Listener
744
745RobotOptions: TypeAlias = _RobotOptions
RobotVariables: TypeAlias = dict[str, object]

variable names and values to be set on the suite level. see the set_variables function

def set_variables(variables: dict[str, object]) -> None:
58def set_variables(variables: RobotVariables) -> None:
59    """
60    sets suite-level variables, equivalent to the `*** Variables ***` section in a `.robot` file.
61
62    also performs some validation checks that robot doesn't to make sure the variable has the
63    correct type matching its prefix.
64    """
65    suite_path = Path(inspect.stack()[1].filename)
66    _suite_variables[suite_path] = variables

sets suite-level variables, equivalent to the *** Variables *** section in a .robot file.

also performs some validation checks that robot doesn't to make sure the variable has the correct type matching its prefix.

def import_resource(path: pathlib._local.Path | str) -> None:
72def import_resource(path: Path | str) -> None:
73    """
74    imports the specified robot `.resource` file when the suite execution begins.
75    use this when specifying robot resource imports at the top of the file.
76
77    to import libraries, use a regular python import
78    """
79    if execution_context():
80        BuiltIn().import_resource(escape_robot_str(str(path)))
81    else:
82        _resources.append(Path(path))

imports the specified robot .resource file when the suite execution begins. use this when specifying robot resource imports at the top of the file.

to import libraries, use a regular python import

def keyword( fn: Optional[Callable[~P, ~T]] = None, *, name: str | None = None, tags: tuple[str, ...] | None = None, module: str | None = None, wrap_context_manager: bool | None = None) -> Union[pytest_robotframework._KeywordDecorator, Callable[~P, ~T]]:
471def keyword(  # pylint:disable=missing-param-doc
472    fn: Callable[P, T] | None = None,
473    *,
474    name: str | None = None,
475    tags: tuple[str, ...] | None = None,
476    module: str | None = None,
477    wrap_context_manager: bool | None = None,
478) -> _KeywordDecorator | Callable[P, T]:
479    """
480    marks a function as a keyword and makes it show in the robot log.
481
482    unlike robot's `deco.keyword` decorator, this one will make your function appear as a keyword in
483    the robot log even when ran from a python file.
484
485    if the function returns a context manager, its body is included in the keyword (just make sure
486    the `@keyword` decorator is above `@contextmanager`)
487
488    :param name: set a custom name for the keyword in the robot log (default is inferred from the
489    decorated function name). equivalent to `robot.api.deco.keyword`'s `name` argument
490    :param tags: equivalent to `robot.api.deco.keyword`'s `tags` argument
491    :param module: customize the module that appears top the left of the keyword name in the log.
492    defaults to the function's actual module
493    :param wrap_context_manager: if the decorated function returns a context manager, whether or not
494    to wrap the context manager instead of the function. you probably always want this to be `True`,
495    unless you don't always intend to use the returned context manager.
496    """
497    if fn is None:
498        if wrap_context_manager is None:
499            return _FunctionKeywordDecorator(name=name, tags=tags, module=module)
500        if wrap_context_manager:
501            return _WrappedContextManagerKeywordDecorator(name=name, tags=tags, module=module)
502        return _NonWrappedContextManagerKeywordDecorator(name=name, tags=tags, module=module)
503    return keyword(  # pyright:ignore[reportReturnType]
504        name=name, tags=tags, module=module, wrap_context_manager=wrap_context_manager
505    )(fn)  # pyright:ignore[reportArgumentType]

marks a function as a keyword and makes it show in the robot log.

unlike robot's deco.keyword decorator, this one will make your function appear as a keyword in the robot log even when ran from a python file.

if the function returns a context manager, its body is included in the keyword (just make sure the @keyword decorator is above @contextmanager)

Parameters
  • name: set a custom name for the keyword in the robot log (default is inferred from the decorated function name). equivalent to robot.api.deco.keyword's name argument
  • tags: equivalent to robot.api.deco.keyword's tags argument
  • module: customize the module that appears top the left of the keyword name in the log. defaults to the function's actual module
  • wrap_context_manager: if the decorated function returns a context manager, whether or not to wrap the context manager instead of the function. you probably always want this to be True, unless you don't always intend to use the returned context manager.
def as_keyword( name: str, *, doc: str = '', tags: tuple[str, ...] | None = None, args: Iterable[str] | None = None, kwargs: Mapping[str, str] | None = None) -> contextlib.AbstractContextManager[None]:
508def as_keyword(
509    name: str,
510    *,
511    doc: str = "",
512    tags: tuple[str, ...] | None = None,
513    args: Iterable[str] | None = None,
514    kwargs: Mapping[str, str] | None = None,
515) -> AbstractContextManager[None]:
516    """
517    runs the body as a robot keyword.
518
519    example:
520    -------
521    >>> with as_keyword("do thing"):
522    ...     ...
523
524    :param name: the name for the keyword
525    :param doc: the documentation to be displayed underneath the keyword in the robot log
526    :param tags: tags for the keyword
527    :param args: positional arguments to be displayed on the keyword in the robot log
528    :param kwargs: keyword arguments to be displayed on the keyword in the robot log
529    """
530
531    @_WrappedContextManagerKeywordDecorator(name=name, tags=tags, doc=doc, module="")
532    @contextmanager
533    def fn(*_args: str, **_kwargs: str) -> Iterator[None]:
534        yield
535
536    return fn(*(args or []), **(kwargs or {}))

runs the body as a robot keyword.

example:

>>> with as_keyword("do thing"):
...     ...
Parameters
  • name: the name for the keyword
  • doc: the documentation to be displayed underneath the keyword in the robot log
  • tags: tags for the keyword
  • args: positional arguments to be displayed on the keyword in the robot log
  • kwargs: keyword arguments to be displayed on the keyword in the robot log
def keywordify( obj: object, method_name: str, *, name: str | None = None, tags: tuple[str, ...] | None = None, module: str | None = None, wrap_context_manager: bool = False) -> None:
539def keywordify(
540    obj: object,
541    method_name: str,
542    *,
543    name: str | None = None,
544    tags: tuple[str, ...] | None = None,
545    module: str | None = None,
546    wrap_context_manager: bool = False,
547) -> None:
548    """
549    patches a function to make it show as a keyword in the robot log.
550
551    you should only use this on third party modules that you don't control. if you want your own
552    function to show as a keyword you should decorate it with `@keyword` instead (the one from this
553    module, not the one from robot)
554
555    :param obj: the object with the method to patch on it (this has to be specified separately as
556    the object itself needs to be modified with the patched method)
557    :param method_name: the name of the method to patch
558    :param name: set a custom name for the keyword in the robot log (default is inferred from the
559    decorated function name). equivalent to `robot.api.deco.keyword`'s `name` argument
560    :param tags: equivalent to `robot.api.deco.keyword`'s `tags` argument
561    :param module: customize the module that appears top the left of the keyword name in the log.
562    defaults to the function's actual module
563    :param wrap_context_manager: if the decorated function returns a context manager, whether or not
564    to wrap the context manager instead of the function. you probably always want this to be `True`,
565    unless you don't always intend to use the returned context manager
566    """
567    setattr(
568        obj,
569        method_name,
570        keyword(name=name, tags=tags, module=module, wrap_context_manager=wrap_context_manager)(
571            getattr(obj, method_name)  # pyright:ignore[reportAny]
572        ),
573    )

patches a function to make it show as a keyword in the robot log.

you should only use this on third party modules that you don't control. if you want your own function to show as a keyword you should decorate it with @keyword instead (the one from this module, not the one from robot)

Parameters
  • obj: the object with the method to patch on it (this has to be specified separately as the object itself needs to be modified with the patched method)
  • method_name: the name of the method to patch
  • name: set a custom name for the keyword in the robot log (default is inferred from the decorated function name). equivalent to robot.api.deco.keyword's name argument
  • tags: equivalent to robot.api.deco.keyword's tags argument
  • module: customize the module that appears top the left of the keyword name in the log. defaults to the function's actual module
  • wrap_context_manager: if the decorated function returns a context manager, whether or not to wrap the context manager instead of the function. you probably always want this to be True, unless you don't always intend to use the returned context manager
def catch_errors(cls: ~_T_ListenerOrSuiteVisitor) -> ~_T_ListenerOrSuiteVisitor:
581def catch_errors(cls: _T_ListenerOrSuiteVisitor) -> _T_ListenerOrSuiteVisitor:
582    """
583    errors that occur inside suite visitors and listeners do not cause the test run to fail. even
584    `--exitonerror` doesn't catch every exception (see <https://github.com/robotframework/robotframework/issues/4853>).
585
586    this decorator will remember any errors that occurred inside listeners and suite visitors, then
587    raise them after robot has finished running.
588
589    you don't need this if you are using the `listener` or `pre_rebot_modifier` decorator, as
590    those decorators use `catch_errors` as well
591    """
592    # prevent classes from being wrapped twice
593    marker = "_catch_errors"
594    if hasattr(cls, marker):
595        return cls
596
597    def wrapped(fn: Callable[P, T]) -> Callable[P, T]:
598        @wraps(fn)
599        def inner(*args: P.args, **kwargs: P.kwargs) -> T:
600            try:
601                return fn(*args, **kwargs)
602            except Exception as e:
603                item_or_session = current_item() or current_session()
604                if not item_or_session:
605                    raise InternalError(
606                        # stack trace isn't showsn so we neewd to include the original error in the
607                        # message as well
608                        f"an error occurred inside {cls.__name__} and failed to get the"
609                        f" current pytest item/session: {e}"
610                    ) from e
611                add_robot_error(item_or_session, str(e))
612                raise
613
614        return inner
615
616    for name, method in cast(
617        list[tuple[str, Function]],
618        inspect.getmembers(
619            cls,
620            predicate=lambda attr: inspect.isfunction(attr)  # pyright:ignore[reportAny]
621            # the wrapper breaks static methods idk why, but we shouldn't need to wrap them anyway
622            # because robot listeners/suite visitors don't call any static/class methods
623            and not isinstance(
624                inspect.getattr_static(cls, attr.__name__), (staticmethod, classmethod)
625            )
626            # only wrap methods that are overwritten on the subclass
627            and attr.__name__ in vars(cls)
628            # don't wrap private/dunder methods since they'll get called by the public ones and we
629            # don't want to duplicate errors
630            and not attr.__name__.startswith("_"),
631        ),
632    ):
633        setattr(cls, name, wrapped(method))
634    setattr(cls, marker, True)
635    return cls

errors that occur inside suite visitors and listeners do not cause the test run to fail. even --exitonerror doesn't catch every exception (see https://github.com/robotframework/robotframework/issues/4853).

this decorator will remember any errors that occurred inside listeners and suite visitors, then raise them after robot has finished running.

you don't need this if you are using the listener or pre_rebot_modifier decorator, as those decorators use catch_errors as well

class AssertOptions:
638class AssertOptions:
639    """
640    pass this as the second argument to an `assert` statement to customize how it appears in the
641    robot log.
642
643    example:
644    -------
645    .. code-block:: python
646
647        assert foo == bar, AssertOptions(
648            log_pass=False, description="checking the value", fail_msg="assertion failed"
649        )
650    """
651
652    def __init__(
653        self,
654        *,
655        log_pass: bool | None = None,
656        description: str | None = None,
657        fail_message: str | None = None,
658    ) -> None:
659        super().__init__()
660        self.log_pass: bool | None = log_pass
661        """whether to display the assertion as a keyword in the robot log when it passes.
662
663        by default, a passing `assert` statement will display in the robot log as long as the
664        following conditions are met:
665        - the `enable_assertion_pass_hook` pytest option is enabled
666        - it is not inside a `hide_asserts_from_robot_log` context manager
667        (see [enabling pytest assertions in the robot log](https://github.com/DetachHead/pytest-robotframework/#enabling-pytest-assertions-in-the-robot-log)).
668        - pytest is not run with the `--no-asserts-in-robot-log` argument
669
670        failing `assert` statements will show as keywords in the log as long as the
671        `enable_assertion_pass_hook` pytest option is enabled. if it's disabled, the assertion error
672        will be logged, but not within a keyword.
673
674        example:
675        -------
676        .. code-block:: python
677
678            # (assuming all of these assertions pass)
679
680            # never displays in the robot log:
681            assert foo == bar, AssertOptions(log_pass=False)
682
683            # always displays in the robot log (as long as the `enable_assertion_pass_hook` pytest
684            # option is enabled):
685            assert foo == bar, AssertOptions(log_pass=True)
686
687            # displays in the robot log as only if all 3 conditions mentioned above are met:
688            assert foo == bar
689        """
690
691        self.description: str | None = description
692        """normally, the asserted expression as it was written is displayed as the argument to the
693        `assert` keyword in the robot log, but setting this value will display a custom message
694        instead. when a custom description is used, the original expression is logged inside the
695        keyword instead."""
696
697        self.fail_message: str | None = fail_message
698        """optional description for the `assert` statement that will be included in the
699        `AssertionError` message if the assertion fails. equivalent to a normal `assert` statement's
700        second argument"""
701
702    @override
703    def __repr__(self) -> str:
704        """make the custom fail message appear in the call to `AssertionError`"""
705        return self.fail_message or ""

pass this as the second argument to an assert statement to customize how it appears in the robot log.

example:

assert foo == bar, AssertOptions(
    log_pass=False, description="checking the value", fail_msg="assertion failed"
)
AssertOptions( *, log_pass: bool | None = None, description: str | None = None, fail_message: str | None = None)
652    def __init__(
653        self,
654        *,
655        log_pass: bool | None = None,
656        description: str | None = None,
657        fail_message: str | None = None,
658    ) -> None:
659        super().__init__()
660        self.log_pass: bool | None = log_pass
661        """whether to display the assertion as a keyword in the robot log when it passes.
662
663        by default, a passing `assert` statement will display in the robot log as long as the
664        following conditions are met:
665        - the `enable_assertion_pass_hook` pytest option is enabled
666        - it is not inside a `hide_asserts_from_robot_log` context manager
667        (see [enabling pytest assertions in the robot log](https://github.com/DetachHead/pytest-robotframework/#enabling-pytest-assertions-in-the-robot-log)).
668        - pytest is not run with the `--no-asserts-in-robot-log` argument
669
670        failing `assert` statements will show as keywords in the log as long as the
671        `enable_assertion_pass_hook` pytest option is enabled. if it's disabled, the assertion error
672        will be logged, but not within a keyword.
673
674        example:
675        -------
676        .. code-block:: python
677
678            # (assuming all of these assertions pass)
679
680            # never displays in the robot log:
681            assert foo == bar, AssertOptions(log_pass=False)
682
683            # always displays in the robot log (as long as the `enable_assertion_pass_hook` pytest
684            # option is enabled):
685            assert foo == bar, AssertOptions(log_pass=True)
686
687            # displays in the robot log as only if all 3 conditions mentioned above are met:
688            assert foo == bar
689        """
690
691        self.description: str | None = description
692        """normally, the asserted expression as it was written is displayed as the argument to the
693        `assert` keyword in the robot log, but setting this value will display a custom message
694        instead. when a custom description is used, the original expression is logged inside the
695        keyword instead."""
696
697        self.fail_message: str | None = fail_message
698        """optional description for the `assert` statement that will be included in the
699        `AssertionError` message if the assertion fails. equivalent to a normal `assert` statement's
700        second argument"""
log_pass: bool | None

whether to display the assertion as a keyword in the robot log when it passes.

by default, a passing assert statement will display in the robot log as long as the following conditions are met:

failing assert statements will show as keywords in the log as long as the enable_assertion_pass_hook pytest option is enabled. if it's disabled, the assertion error will be logged, but not within a keyword.

example:

# (assuming all of these assertions pass)

# never displays in the robot log:
assert foo == bar, AssertOptions(log_pass=False)

# always displays in the robot log (as long as the `enable_assertion_pass_hook` pytest
# option is enabled):
assert foo == bar, AssertOptions(log_pass=True)

# displays in the robot log as only if all 3 conditions mentioned above are met:
assert foo == bar
description: str | None

normally, the asserted expression as it was written is displayed as the argument to the assert keyword in the robot log, but setting this value will display a custom message instead. when a custom description is used, the original expression is logged inside the keyword instead.

fail_message: str | None

optional description for the assert statement that will be included in the AssertionError message if the assertion fails. equivalent to a normal assert statement's second argument

@contextmanager
def hide_asserts_from_robot_log() -> Iterator[None]:
711@contextmanager
712def hide_asserts_from_robot_log() -> Iterator[None]:
713    """
714    context manager for hiding multiple passing `assert` statements from the robot log. note that
715    individual `assert` statements using `AssertOptions(log_pass=True)` take precedence, and that
716    failing assertions will always appear in the log.
717
718    when hiding only a single `assert` statement, you should use `AssertOptions(log=False)` instead.
719
720    example:
721    -------
722    .. code-block:: python
723
724        assert True  # not hidden
725        with hide_asserts_from_robot_log():
726            assert True  # hidden
727            assert True, AssertOptions(log_pass=True)  # not hidden
728    """
729    item = current_item()
730    if not item:
731        raise InternalError(
732            f"failed to get current pytest item in {hide_asserts_from_robot_log.__name__}"
733        )
734    previous_value = item.stash.get(_hide_asserts_context_manager_key, False)
735    item.stash[_hide_asserts_context_manager_key] = True
736    try:
737        yield
738    finally:
739        item.stash[_hide_asserts_context_manager_key] = previous_value

context manager for hiding multiple passing assert statements from the robot log. note that individual assert statements using AssertOptions(log_pass=True) take precedence, and that failing assertions will always appear in the log.

when hiding only a single assert statement, you should use AssertOptions(log=False) instead.

example:

assert True  # not hidden
with hide_asserts_from_robot_log():
    assert True  # hidden
    assert True, AssertOptions(log_pass=True)  # not hidden
Listener: TypeAlias = Union[robot.api.interfaces.ListenerV2, robot.api.interfaces.ListenerV3]
class RobotOptions(typing.TypedDict):
 56class RobotOptions(TypedDict):
 57    """
 58    robot command-line arguments after being parsed by robot into a `dict`.
 59
 60    for example, the following robot options:
 61
 62    ```dotenv
 63    ROBOT_OPTIONS="--listener Foo --listener Bar -d baz"
 64    ```
 65
 66    will be converted to a `dict` like so:
 67    >>> {"listener": ["Foo", "Bar"], "outputdir": "baz"}
 68
 69    any options missing from this `TypedDict` are not allowed to be modified as they interfere with
 70    the functionality of this plugin. see https://github.com/detachhead/pytest-robotframework#config
 71    for alternatives
 72    """
 73
 74    rpa: bool | None
 75    language: str | None
 76    extension: str
 77    name: str | None
 78    doc: str | None
 79    metadata: list[str]
 80    settag: list[str]
 81    rerunfailedsuites: list[str] | None
 82    skiponfailure: list[str]
 83    variable: list[str]
 84    variablefile: list[str]
 85    outputdir: str
 86    output: str | None
 87    log: str | None
 88    report: str | None
 89    xunit: str | None
 90    debugfile: str | None
 91    timestampoutputs: bool
 92    splitlog: bool
 93    logtitle: str | None
 94    reporttitle: str | None
 95    reportbackground: tuple[str, str] | tuple[str, str, str]
 96    maxerrorlines: int | None
 97    maxassignlength: int
 98    loglevel: str
 99    suitestatlevel: int
100    tagstatinclude: list[str]
101    tagstatexclude: list[str]
102    tagstatcombine: list[str]
103    tagdoc: list[str]
104    tagstatlink: list[str]
105    expandkeywords: list[str]
106    removekeywords: list[str]
107    flattenkeywords: list[str]
108    listener: list[str | Listener]
109    statusrc: bool
110    skipteardownonexit: bool
111    prerunmodifier: list[str | model.SuiteVisitor]
112    prerebotmodifier: list[str | model.SuiteVisitor]
113    randomize: Literal["ALL", "SUITES", "TESTS", "NONE"]
114    console: Literal["verbose", "dotted", "quiet", "none"]
115    """the default in robot is `"verbose", however pytest-robotframework changes the default to
116    `"quiet"`, if you change this, then pytest and robot outputs will overlap."""
117    dotted: bool
118    quiet: bool
119    consolewidth: int
120    consolecolors: Literal["AUTO", "ON", "ANSI", "OFF"]
121    consolelinks: Literal["AUTO", "OFF"]
122    """only available in robotframework >=7.1.
123    
124    currently does nothing. see https://github.com/DetachHead/pytest-robotframework/issues/305"""
125    consolemarkers: Literal["AUTO", "ON", "OFF"]
126    pythonpath: list[str]
127    # argumentfile is not supported because it's not in the _cli_opts dict for some reason
128    # argumentfile: str | None  # noqa: ERA001
129    parser: list[str | Parser]
130    legacyoutput: bool
131    parseinclude: list[str]
132    stdout: object  # no idea what this is, it's not in the robot docs
133    stderr: object  # no idea what this is, it's not in the robot docs
134    exitonerror: bool

robot command-line arguments after being parsed by robot into a dict.

for example, the following robot options:

ROBOT_OPTIONS="--listener Foo --listener Bar -d baz"

will be converted to a dict like so:

>>> {"listener": ["Foo", "Bar"], "outputdir": "baz"}

any options missing from this TypedDict are not allowed to be modified as they interfere with the functionality of this plugin. see https://github.com/detachhead/pytest-robotframework#config for alternatives

rpa: bool | None
language: str | None
extension: str
name: str | None
doc: str | None
metadata: list[str]
settag: list[str]
rerunfailedsuites: list[str] | None
skiponfailure: list[str]
variable: list[str]
variablefile: list[str]
outputdir: str
output: str | None
log: str | None
report: str | None
xunit: str | None
debugfile: str | None
timestampoutputs: bool
splitlog: bool
logtitle: str | None
reporttitle: str | None
reportbackground: tuple[str, str] | tuple[str, str, str]
maxerrorlines: int | None
maxassignlength: int
loglevel: str
suitestatlevel: int
tagstatinclude: list[str]
tagstatexclude: list[str]
tagstatcombine: list[str]
tagdoc: list[str]
expandkeywords: list[str]
removekeywords: list[str]
flattenkeywords: list[str]
listener: list[typing.Union[str, robot.api.interfaces.ListenerV2, robot.api.interfaces.ListenerV3]]
statusrc: bool
skipteardownonexit: bool
prerunmodifier: list[str | robot.model.visitor.SuiteVisitor]
prerebotmodifier: list[str | robot.model.visitor.SuiteVisitor]
randomize: Literal['ALL', 'SUITES', 'TESTS', 'NONE']
console: Literal['verbose', 'dotted', 'quiet', 'none']

the default in robot is "verbose", however pytest-robotframework changes the default to "quiet"`, if you change this, then pytest and robot outputs will overlap.

dotted: bool
quiet: bool
consolewidth: int
consolecolors: Literal['AUTO', 'ON', 'ANSI', 'OFF']
consolemarkers: Literal['AUTO', 'ON', 'OFF']
pythonpath: list[str]
parser: list[str | robot.api.interfaces.Parser]
legacyoutput: bool
parseinclude: list[str]
stdout: object
stderr: object
exitonerror: bool