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