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

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]:
506def as_keyword(
507    name: str,
508    *,
509    doc: str = "",
510    tags: tuple[str, ...] | None = None,
511    args: Iterable[str] | None = None,
512    kwargs: Mapping[str, str] | None = None,
513) -> AbstractContextManager[None]:
514    """runs the body as a robot keyword.
515
516    example:
517    -------
518    >>> with as_keyword("do thing"):
519    ...     ...
520
521    :param name: the name for the keyword
522    :param doc: the documentation to be displayed underneath the keyword in the robot log
523    :param tags: tags for the keyword
524    :param args: positional arguments to be displayed on the keyword in the robot log
525    :param kwargs: keyword arguments to be displayed on the keyword in the robot log
526    """
527
528    @_WrappedContextManagerKeywordDecorator(name=name, tags=tags, doc=doc, module="")
529    @contextmanager
530    def fn(*_args: str, **_kwargs: str) -> Iterator[None]:
531        yield
532
533    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:
536def keywordify(
537    obj: object,
538    method_name: str,
539    *,
540    name: str | None = None,
541    tags: tuple[str, ...] | None = None,
542    module: str | None = None,
543    wrap_context_manager: bool = False,
544) -> None:
545    """patches a function to make it show as a keyword in the robot log.
546
547    you should only use this on third party modules that you don't control. if you want your own
548    function to show as a keyword you should decorate it with `@keyword` instead (the one from this
549    module, not the one from robot)
550
551    :param obj: the object with the method to patch on it (this has to be specified separately as
552    the object itself needs to be modified with the patched method)
553    :param method_name: the name of the method to patch
554    :param name: set a custom name for the keyword in the robot log (default is inferred from the
555    decorated function name). equivalent to `robot.api.deco.keyword`'s `name` argument
556    :param tags: equivalent to `robot.api.deco.keyword`'s `tags` argument
557    :param module: customize the module that appears top the left of the keyword name in the log.
558    defaults to the function's actual module
559    :param wrap_context_manager: if the decorated function returns a context manager, whether or not
560    to wrap the context manager instead of the function. you probably always want this to be `True`,
561    unless you don't always intend to use the returned context manager
562    """
563    setattr(
564        obj,
565        method_name,
566        keyword(  # pyright:ignore[reportCallIssue]
567            name=name,
568            tags=tags,
569            module=module,
570            wrap_context_manager=wrap_context_manager,  # pyright:ignore[reportArgumentType]
571        )(getattr(obj, method_name)),
572    )

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:
580def catch_errors(cls: _T_ListenerOrSuiteVisitor) -> _T_ListenerOrSuiteVisitor:
581    """errors that occur inside suite visitors and listeners do not cause the test run to fail. even
582    `--exitonerror` doesn't catch every exception (see <https://github.com/robotframework/robotframework/issues/4853>).
583
584    this decorator will remember any errors that occurred inside listeners and suite visitors, then
585    raise them after robot has finished running.
586
587    you don't need this if you are using the `listener` or `pre_rebot_modifier` decorator, as
588    those decorators use `catch_errors` as well"""
589    # prevent classes from being wrapped twice
590    marker = "_catch_errors"
591    if hasattr(cls, marker):
592        return cls
593
594    def wrapped(fn: Callable[P, T]) -> Callable[P, T]:
595        @wraps(fn)
596        def inner(*args: P.args, **kwargs: P.kwargs) -> T:
597            try:
598                return fn(*args, **kwargs)
599            except Exception as e:
600                item_or_session = current_item() or current_session()
601                if not item_or_session:
602                    raise InternalError(
603                        # stack trace isn't showsn so we neewd to include the original error in the
604                        # message as well
605                        f"an error occurred inside {cls.__name__} and failed to get the"
606                        f" current pytest item/session: {e}"
607                    ) from e
608                add_robot_error(item_or_session, str(e))
609                raise
610
611        return inner
612
613    for name, method in cast(
614        list[tuple[str, Function]],
615        inspect.getmembers(
616            cls,
617            predicate=lambda attr: inspect.isfunction(attr)  # pyright:ignore[reportAny]
618            # the wrapper breaks static methods idk why, but we shouldn't need to wrap them anyway
619            # because robot listeners/suite visitors don't call any static/class methods
620            and not isinstance(
621                inspect.getattr_static(cls, attr.__name__), (staticmethod, classmethod)
622            )
623            # only wrap methods that are overwritten on the subclass
624            and attr.__name__ in vars(cls)
625            # don't wrap private/dunder methods since they'll get called by the public ones and we
626            # don't want to duplicate errors
627            and not attr.__name__.startswith("_"),
628        ),
629    ):
630        setattr(cls, name, wrapped(method))
631    setattr(cls, marker, True)
632    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:
635class AssertOptions:
636    """pass this as the second argument to an `assert` statement to customize how it appears in the
637    robot log.
638
639    example:
640    -------
641    .. code-block:: python
642
643        assert foo == bar, AssertOptions(
644            log_pass=False, description="checking the value", fail_msg="assertion failed"
645        )
646    """
647
648    def __init__(
649        self,
650        *,
651        log_pass: bool | None = None,
652        description: str | None = None,
653        fail_message: str | None = None,
654    ) -> None:
655        super().__init__()
656        self.log_pass: bool | None = log_pass
657        """whether to display the assertion as a keyword in the robot log when it passes.
658
659        by default, a passing `assert` statement will display in the robot log as long as the
660        following conditions are met:
661        - the `enable_assertion_pass_hook` pytest option is enabled
662        - it is not inside a `hide_asserts_from_robot_log` context manager
663        (see [enabling pytest assertions in the robot log](https://github.com/DetachHead/pytest-robotframework/#enabling-pytest-assertions-in-the-robot-log)).
664        - pytest is not run with the `--no-asserts-in-robot-log` argument
665
666        failing `assert` statements will show as keywords in the log as long as the
667        `enable_assertion_pass_hook` pytest option is enabled. if it's disabled, the assertion error
668        will be logged, but not within a keyword.
669
670        example:
671        -------
672        .. code-block:: python
673
674            # (assuming all of these assertions pass)
675
676            # never displays in the robot log:
677            assert foo == bar, AssertOptions(log_pass=False)
678
679            # always displays in the robot log (as long as the `enable_assertion_pass_hook` pytest
680            # option is enabled):
681            assert foo == bar, AssertOptions(log_pass=True)
682
683            # displays in the robot log as only if all 3 conditions mentioned above are met:
684            assert foo == bar
685        """
686
687        self.description: str | None = description
688        """normally, the asserted expression as it was written is displayed as the argument to the
689        `assert` keyword in the robot log, but setting this value will display a custom message
690        instead. when a custom description is used, the original expression is logged inside the
691        keyword instead."""
692
693        self.fail_message: str | None = fail_message
694        """optional description for the `assert` statement that will be included in the
695        `AssertionError` message if the assertion fails. equivalent to a normal `assert` statement's
696        second argument"""
697
698    @override
699    def __repr__(self) -> str:
700        """make the custom fail message appear in the call to `AssertionError`"""
701        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)
648    def __init__(
649        self,
650        *,
651        log_pass: bool | None = None,
652        description: str | None = None,
653        fail_message: str | None = None,
654    ) -> None:
655        super().__init__()
656        self.log_pass: bool | None = log_pass
657        """whether to display the assertion as a keyword in the robot log when it passes.
658
659        by default, a passing `assert` statement will display in the robot log as long as the
660        following conditions are met:
661        - the `enable_assertion_pass_hook` pytest option is enabled
662        - it is not inside a `hide_asserts_from_robot_log` context manager
663        (see [enabling pytest assertions in the robot log](https://github.com/DetachHead/pytest-robotframework/#enabling-pytest-assertions-in-the-robot-log)).
664        - pytest is not run with the `--no-asserts-in-robot-log` argument
665
666        failing `assert` statements will show as keywords in the log as long as the
667        `enable_assertion_pass_hook` pytest option is enabled. if it's disabled, the assertion error
668        will be logged, but not within a keyword.
669
670        example:
671        -------
672        .. code-block:: python
673
674            # (assuming all of these assertions pass)
675
676            # never displays in the robot log:
677            assert foo == bar, AssertOptions(log_pass=False)
678
679            # always displays in the robot log (as long as the `enable_assertion_pass_hook` pytest
680            # option is enabled):
681            assert foo == bar, AssertOptions(log_pass=True)
682
683            # displays in the robot log as only if all 3 conditions mentioned above are met:
684            assert foo == bar
685        """
686
687        self.description: str | None = description
688        """normally, the asserted expression as it was written is displayed as the argument to the
689        `assert` keyword in the robot log, but setting this value will display a custom message
690        instead. when a custom description is used, the original expression is logged inside the
691        keyword instead."""
692
693        self.fail_message: str | None = fail_message
694        """optional description for the `assert` statement that will be included in the
695        `AssertionError` message if the assertion fails. equivalent to a normal `assert` statement's
696        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]:
707@contextmanager
708def hide_asserts_from_robot_log() -> Iterator[None]:
709    """context manager for hiding multiple passing `assert` statements from the robot log. note that
710    individual `assert` statements using `AssertOptions(log_pass=True)` take precedence, and that
711    failing assertions will always appear in the log.
712
713    when hiding only a single `assert` statement, you should use `AssertOptions(log=False)` instead.
714
715    example:
716    -------
717    .. code-block:: python
718
719        assert True  # not hidden
720        with hide_asserts_from_robot_log():
721            assert True  # hidden
722            assert True, AssertOptions(log_pass=True)  # not hidden
723    """
724    item = current_item()
725    if not item:
726        raise InternalError(
727            f"failed to get current pytest item in {hide_asserts_from_robot_log.__name__}"
728        )
729    previous_value = item.stash.get(_hide_asserts_context_manager_key, False)
730    item.stash[_hide_asserts_context_manager_key] = True
731    try:
732        yield
733    finally:
734        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):
 54class RobotOptions(TypedDict):
 55    """robot command-line arguments after being parsed by robot into a `dict`.
 56
 57    for example, the following robot options:
 58
 59    ```dotenv
 60    ROBOT_OPTIONS="--listener Foo --listener Bar -d baz"
 61    ```
 62
 63    will be converted to a `dict` like so:
 64    >>> {"listener": ["Foo", "Bar"], "outputdir": "baz"}
 65
 66    any options missing from this `TypedDict` are not allowed to be modified as they interfere with
 67    the functionality of this plugin. see https://github.com/detachhead/pytest-robotframework#config
 68    for alternatives
 69    """
 70
 71    rpa: bool | None
 72    language: str | None
 73    extension: str
 74    name: str | None
 75    doc: str | None
 76    metadata: list[str]
 77    settag: list[str]
 78    rerunfailedsuites: list[str] | None
 79    skiponfailure: list[str]
 80    variable: list[str]
 81    variablefile: list[str]
 82    outputdir: str
 83    output: str | None
 84    log: str | None
 85    report: str | None
 86    xunit: str | None
 87    debugfile: str | None
 88    timestampoutputs: bool
 89    splitlog: bool
 90    logtitle: str | None
 91    reporttitle: str | None
 92    reportbackground: tuple[str, str] | tuple[str, str, str]
 93    maxerrorlines: int | None
 94    maxassignlength: int
 95    loglevel: str
 96    suitestatlevel: int
 97    tagstatinclude: list[str]
 98    tagstatexclude: list[str]
 99    tagstatcombine: list[str]
100    tagdoc: list[str]
101    tagstatlink: list[str]
102    expandkeywords: list[str]
103    removekeywords: list[str]
104    flattenkeywords: list[str]
105    listener: list[str | Listener]
106    statusrc: bool
107    skipteardownonexit: bool
108    prerunmodifier: list[str | model.SuiteVisitor]
109    prerebotmodifier: list[str | model.SuiteVisitor]
110    randomize: Literal["ALL", "SUITES", "TESTS", "NONE"]
111    console: Literal["verbose", "dotted", "quiet", "none"]
112    """the default in robot is `"verbose", however pytest-robotframework changes the default to
113    `"quiet"`, if you change this, then pytest and robot outputs will overlap."""
114    dotted: bool
115    quiet: bool
116    consolewidth: int
117    consolecolors: Literal["AUTO", "ON", "ANSI", "OFF"]
118    consolelinks: Literal["AUTO", "OFF"]
119    """only available in robotframework >=7.1.
120    
121    currently does nothing. see https://github.com/DetachHead/pytest-robotframework/issues/305"""
122    consolemarkers: Literal["AUTO", "ON", "OFF"]
123    pythonpath: list[str]
124    # argumentfile is not supported because it's not in the _cli_opts dict for some reason
125    # argumentfile: str | None  # noqa: ERA001
126    parser: list[str | Parser]
127    legacyoutput: bool
128    parseinclude: list[str]
129    stdout: object  # no idea what this is, it's not in the robot docs
130    stderr: object  # no idea what this is, it's not in the robot docs
131    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