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