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

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:
562def catch_errors(cls: _T_ListenerOrSuiteVisitor) -> _T_ListenerOrSuiteVisitor:
563    """errors that occur inside suite visitors and listeners do not cause the test run to fail. even
564    `--exitonerror` doesn't catch every exception (see <https://github.com/robotframework/robotframework/issues/4853>).
565
566    this decorator will remember any errors that occurred inside listeners and suite visitors, then
567    raise them after robot has finished running.
568
569    you don't need this if you are using the `listener` or `pre_rebot_modifier` decorator, as
570    those decorators use `catch_errors` as well"""
571    # prevent classes from being wrapped twice
572    marker = "_catch_errors"
573    if hasattr(cls, marker):
574        return cls
575
576    def wrapped(fn: Callable[P, T]) -> Callable[P, T]:
577        @wraps(fn)
578        def inner(*args: P.args, **kwargs: P.kwargs) -> T:
579            try:
580                return fn(*args, **kwargs)
581            except Exception as e:
582                item_or_session = current_item() or current_session()
583                if not item_or_session:
584                    raise InternalError(
585                        # stack trace isn't showsn so we neewd to include the original error in the
586                        # message as well
587                        f"an error occurred inside {cls.__name__} and failed to get the"
588                        f" current pytest item/session: {e}"
589                    ) from e
590                add_robot_error(item_or_session, str(e))
591                raise
592
593        return inner
594
595    for name, method in cast(
596        list[tuple[str, Function]],
597        inspect.getmembers(
598            cls,
599            predicate=lambda attr: inspect.isfunction(attr)  # pyright:ignore[reportAny]
600            # the wrapper breaks static methods idk why, but we shouldn't need to wrap them anyway
601            # because robot listeners/suite visitors don't call any static/class methods
602            and not isinstance(
603                inspect.getattr_static(cls, attr.__name__), (staticmethod, classmethod)
604            )
605            # only wrap methods that are overwritten on the subclass
606            and attr.__name__ in vars(cls)
607            # don't wrap private/dunder methods since they'll get called by the public ones and we
608            # don't want to duplicate errors
609            and not attr.__name__.startswith("_"),
610        ),
611    ):
612        setattr(cls, name, wrapped(method))
613    setattr(cls, marker, True)
614    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:
617class AssertOptions:
618    """pass this as the second argument to an `assert` statement to customize how it appears in the
619    robot log.
620
621    example:
622    -------
623    .. code-block:: python
624
625        assert foo == bar, AssertOptions(
626            log_pass=False, description="checking the value", fail_msg="assertion failed"
627        )
628    """
629
630    def __init__(
631        self,
632        *,
633        log_pass: bool | None = None,
634        description: str | None = None,
635        fail_message: str | None = None,
636    ) -> None:
637        super().__init__()
638        self.log_pass: bool | None = log_pass
639        """whether to display the assertion as a keyword in the robot log when it passes.
640
641        by default, a passing `assert` statement will display in the robot log as long as the
642        following conditions are met:
643        - the `enable_assertion_pass_hook` pytest option is enabled
644        - it is not inside a `hide_asserts_from_robot_log` context manager
645        (see [enabling pytest assertions in the robot log](https://github.com/DetachHead/pytest-robotframework/#enabling-pytest-assertions-in-the-robot-log)).
646        - pytest is not run with the `--no-asserts-in-robot-log` argument
647
648        failing `assert` statements will show as keywords in the log as long as the
649        `enable_assertion_pass_hook` pytest option is enabled. if it's disabled, the assertion error
650        will be logged, but not within a keyword.
651
652        example:
653        -------
654        .. code-block:: python
655
656            # (assuming all of these assertions pass)
657
658            # never displays in the robot log:
659            assert foo == bar, AssertOptions(log_pass=False)
660
661            # always displays in the robot log (as long as the `enable_assertion_pass_hook` pytest
662            # option is enabled):
663            assert foo == bar, AssertOptions(log_pass=True)
664
665            # displays in the robot log as only if all 3 conditions mentioned above are met:
666            assert foo == bar
667        """
668
669        self.description: str | None = description
670        """normally, the asserted expression as it was written is displayed as the argument to the
671        `assert` keyword in the robot log, but setting this value will display a custom message
672        instead. when a custom description is used, the original expression is logged inside the
673        keyword instead."""
674
675        self.fail_message: str | None = fail_message
676        """optional description for the `assert` statement that will be included in the
677        `AssertionError` message if the assertion fails. equivalent to a normal `assert` statement's
678        second argument"""
679
680    @override
681    def __repr__(self) -> str:
682        """make the custom fail message appear in the call to `AssertionError`"""
683        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)
630    def __init__(
631        self,
632        *,
633        log_pass: bool | None = None,
634        description: str | None = None,
635        fail_message: str | None = None,
636    ) -> None:
637        super().__init__()
638        self.log_pass: bool | None = log_pass
639        """whether to display the assertion as a keyword in the robot log when it passes.
640
641        by default, a passing `assert` statement will display in the robot log as long as the
642        following conditions are met:
643        - the `enable_assertion_pass_hook` pytest option is enabled
644        - it is not inside a `hide_asserts_from_robot_log` context manager
645        (see [enabling pytest assertions in the robot log](https://github.com/DetachHead/pytest-robotframework/#enabling-pytest-assertions-in-the-robot-log)).
646        - pytest is not run with the `--no-asserts-in-robot-log` argument
647
648        failing `assert` statements will show as keywords in the log as long as the
649        `enable_assertion_pass_hook` pytest option is enabled. if it's disabled, the assertion error
650        will be logged, but not within a keyword.
651
652        example:
653        -------
654        .. code-block:: python
655
656            # (assuming all of these assertions pass)
657
658            # never displays in the robot log:
659            assert foo == bar, AssertOptions(log_pass=False)
660
661            # always displays in the robot log (as long as the `enable_assertion_pass_hook` pytest
662            # option is enabled):
663            assert foo == bar, AssertOptions(log_pass=True)
664
665            # displays in the robot log as only if all 3 conditions mentioned above are met:
666            assert foo == bar
667        """
668
669        self.description: str | None = description
670        """normally, the asserted expression as it was written is displayed as the argument to the
671        `assert` keyword in the robot log, but setting this value will display a custom message
672        instead. when a custom description is used, the original expression is logged inside the
673        keyword instead."""
674
675        self.fail_message: str | None = fail_message
676        """optional description for the `assert` statement that will be included in the
677        `AssertionError` message if the assertion fails. equivalent to a normal `assert` statement's
678        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]:
689@contextmanager
690def hide_asserts_from_robot_log() -> Iterator[None]:
691    """context manager for hiding multiple passing `assert` statements from the robot log. note that
692    individual `assert` statements using `AssertOptions(log_pass=True)` take precedence, and that
693    failing assertions will always appear in the log.
694
695    when hiding only a single `assert` statement, you should use `AssertOptions(log=False)` instead.
696
697    example:
698    -------
699    .. code-block:: python
700
701        assert True  # not hidden
702        with hide_asserts_from_robot_log():
703            assert True  # hidden
704            assert True, AssertOptions(log_pass=True)  # not hidden
705    """
706    item = current_item()
707    if not item:
708        raise InternalError(
709            f"failed to get current pytest item in {hide_asserts_from_robot_log.__name__}"
710        )
711    previous_value = item.stash.get(_hide_asserts_context_manager_key, False)
712    item.stash[_hide_asserts_context_manager_key] = True
713    try:
714        yield
715    finally:
716        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