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

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

def set_variables(variables: Dict[str, object]) -> None:
75def set_variables(variables: RobotVariables) -> None:
76    """sets suite-level variables, equivalent to the `*** Variables ***` section in a `.robot` file.
77
78    also performs some validation checks that robot doesn't to make sure the variable has the
79    correct type matching its prefix."""
80    suite_path = Path(inspect.stack()[1].filename)
81    _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:
87def import_resource(path: Path | str) -> None:
88    """imports the specified robot `.resource` file when the suite execution begins.
89    use this when specifying robot resource imports at the top of the file.
90
91    to import libraries, use a regular python import"""
92    if execution_context():
93        BuiltIn().import_resource(  # pyright:ignore[reportUnknownMemberType]
94            escape_robot_str(str(path))
95        )
96    else:
97        _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]]:
472def keyword(  # pylint:disable=missing-param-doc
473    fn: Callable[P, T] | None = None,
474    *,
475    name: str | None = None,
476    tags: tuple[str, ...] | None = None,
477    module: str | None = None,
478    wrap_context_manager: bool | None = None,
479) -> _KeywordDecorator | Callable[P, T]:
480    """marks a function as a keyword and makes it show in the robot log.
481
482    unlike robot's `deco.keyword` decorator, this one will make your function appear as a keyword in
483    the robot log even when ran from a python file.
484
485    if the function returns a context manager, its body is included in the keyword (just make sure
486    the `@keyword` decorator is above `@contextmanager`)
487
488    :param name: set a custom name for the keyword in the robot log (default is inferred from the
489    decorated function name). equivalent to `robot.api.deco.keyword`'s `name` argument
490    :param tags: equivalent to `robot.api.deco.keyword`'s `tags` argument
491    :param module: customize the module that appears top the left of the keyword name in the log.
492    defaults to the function's actual module
493    :param wrap_context_manager: if the decorated function returns a context manager, whether or not
494    to wrap the context manager instead of the function. you probably always want this to be `True`,
495    unless you don't always intend to use the returned context manager.
496    """
497    if fn is None:
498        if wrap_context_manager is None:
499            return _FunctionKeywordDecorator(name=name, tags=tags, module=module)
500        if wrap_context_manager:
501            return _WrappedContextManagerKeywordDecorator(name=name, tags=tags, module=module)
502        return _NonWrappedContextManagerKeywordDecorator(name=name, tags=tags, module=module)
503    return keyword(  # pyright:ignore[reportCallIssue,reportUnknownVariableType]
504        name=name,
505        tags=tags,
506        module=module,
507        wrap_context_manager=wrap_context_manager,  # pyright:ignore[reportArgumentType]
508    )(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: Optional[Iterable[str]] = None, kwargs: Optional[Mapping[str, str]] = None) -> ContextManager[NoneType]:
511def as_keyword(
512    name: str,
513    *,
514    doc: str = "",
515    tags: tuple[str, ...] | None = None,
516    args: Iterable[str] | None = None,
517    kwargs: Mapping[str, str] | None = None,
518) -> ContextManager[None]:
519    """runs the body as a robot keyword.
520
521    example:
522    -------
523    >>> with as_keyword("do thing"):
524    ...     ...
525
526    :param name: the name for the keyword
527    :param doc: the documentation to be displayed underneath the keyword in the robot log
528    :param tags: tags for the keyword
529    :param args: positional arguments to be displayed on the keyword in the robot log
530    :param kwargs: keyword arguments to be displayed on the keyword in the robot log
531    """
532
533    @_WrappedContextManagerKeywordDecorator(name=name, tags=tags, doc=doc, module="")
534    @contextmanager
535    def fn(*_args: str, **_kwargs: str) -> Iterator[None]:
536        yield
537
538    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:
541def keywordify(
542    obj: object,
543    method_name: str,
544    *,
545    name: str | None = None,
546    tags: tuple[str, ...] | None = None,
547    module: str | None = None,
548    wrap_context_manager: bool = False,
549) -> None:
550    """patches a function to make it show as a keyword in the robot log.
551
552    you should only use this on third party modules that you don't control. if you want your own
553    function to show as a keyword you should decorate it with `@keyword` instead (the one from this
554    module, not the one from robot)
555
556    :param obj: the object with the method to patch on it (this has to be specified separately as
557    the object itself needs to be modified with the patched method)
558    :param method_name: the name of the method to patch
559    :param name: set a custom name for the keyword in the robot log (default is inferred from the
560    decorated function name). equivalent to `robot.api.deco.keyword`'s `name` argument
561    :param tags: equivalent to `robot.api.deco.keyword`'s `tags` argument
562    :param module: customize the module that appears top the left of the keyword name in the log.
563    defaults to the function's actual module
564    :param wrap_context_manager: if the decorated function returns a context manager, whether or not
565    to wrap the context manager instead of the function. you probably always want this to be `True`,
566    unless you don't always intend to use the returned context manager
567    """
568    setattr(
569        obj,
570        method_name,
571        keyword(  # pyright:ignore[reportCallIssue]
572            name=name,
573            tags=tags,
574            module=module,
575            wrap_context_manager=wrap_context_manager,  # pyright:ignore[reportArgumentType]
576        )(getattr(obj, method_name)),
577    )

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

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

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

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

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

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

example:

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