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