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