Skip to content

Commit

Permalink
fix crash with keyword decorator on class libraries
Browse files Browse the repository at this point in the history
  • Loading branch information
DetachHead committed Oct 11, 2024
1 parent 047d385 commit 4059a68
Show file tree
Hide file tree
Showing 5 changed files with 76 additions and 8 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,17 @@
from functools import wraps
from inspect import getdoc
from re import sub
from typing import TYPE_CHECKING, Callable, Final, Literal, Optional, cast
from types import MethodType
from typing import (
TYPE_CHECKING,
Callable,
Final,
Literal,
Optional,
Tuple,

Check failure on line 18 in pytest_robotframework/_internal/robot/listeners_and_suite_visitors.py

View workflow job for this annotation

GitHub Actions / static_checks (3.9, ubuntu-latest)

This type is deprecated as of Python 3.9; use "tuple" instead (reportDeprecated)

Check failure on line 18 in pytest_robotframework/_internal/robot/listeners_and_suite_visitors.py

View workflow job for this annotation

GitHub Actions / static_checks (3.9, ubuntu-latest)

Ruff (F401)

pytest_robotframework/_internal/robot/listeners_and_suite_visitors.py:18:5: F401 `typing.Tuple` imported but unused

Check failure on line 18 in pytest_robotframework/_internal/robot/listeners_and_suite_visitors.py

View workflow job for this annotation

GitHub Actions / static_checks (3.9, macos-latest)

Ruff (F401)

pytest_robotframework/_internal/robot/listeners_and_suite_visitors.py:18:5: F401 `typing.Tuple` imported but unused

Check failure on line 18 in pytest_robotframework/_internal/robot/listeners_and_suite_visitors.py

View workflow job for this annotation

GitHub Actions / static_checks (3.9, macos-latest)

This type is deprecated as of Python 3.9; use "tuple" instead (reportDeprecated)

Check failure on line 18 in pytest_robotframework/_internal/robot/listeners_and_suite_visitors.py

View workflow job for this annotation

GitHub Actions / static_checks (3.9, windows-latest)

Ruff (F401)

pytest_robotframework\_internal\robot\listeners_and_suite_visitors.py:18:5: F401 `typing.Tuple` imported but unused

Check failure on line 18 in pytest_robotframework/_internal/robot/listeners_and_suite_visitors.py

View workflow job for this annotation

GitHub Actions / static_checks (3.9, windows-latest)

This type is deprecated as of Python 3.9; use "tuple" instead (reportDeprecated)

Check failure on line 18 in pytest_robotframework/_internal/robot/listeners_and_suite_visitors.py

View workflow job for this annotation

GitHub Actions / static_checks (3.13.0-rc.3, ubuntu-latest)

This type is deprecated as of Python 3.9; use "tuple" instead (reportDeprecated)

Check failure on line 18 in pytest_robotframework/_internal/robot/listeners_and_suite_visitors.py

View workflow job for this annotation

GitHub Actions / static_checks (3.13.0-rc.3, ubuntu-latest)

Ruff (F401)

pytest_robotframework/_internal/robot/listeners_and_suite_visitors.py:18:5: F401 `typing.Tuple` imported but unused

Check failure on line 18 in pytest_robotframework/_internal/robot/listeners_and_suite_visitors.py

View workflow job for this annotation

GitHub Actions / static_checks (3.13.0-rc.3, macos-latest)

This type is deprecated as of Python 3.9; use "tuple" instead (reportDeprecated)

Check failure on line 18 in pytest_robotframework/_internal/robot/listeners_and_suite_visitors.py

View workflow job for this annotation

GitHub Actions / static_checks (3.13.0-rc.3, macos-latest)

Ruff (F401)

pytest_robotframework/_internal/robot/listeners_and_suite_visitors.py:18:5: F401 `typing.Tuple` imported but unused

Check failure on line 18 in pytest_robotframework/_internal/robot/listeners_and_suite_visitors.py

View workflow job for this annotation

GitHub Actions / static_checks (3.13.0-rc.3, windows-latest)

This type is deprecated as of Python 3.9; use "tuple" instead (reportDeprecated)

Check failure on line 18 in pytest_robotframework/_internal/robot/listeners_and_suite_visitors.py

View workflow job for this annotation

GitHub Actions / static_checks (3.13.0-rc.3, windows-latest)

Ruff (F401)

pytest_robotframework\_internal\robot\listeners_and_suite_visitors.py:18:5: F401 `typing.Tuple` imported but unused
TypeVar,
cast,
)

from _pytest import runner
from _pytest.python import PyobjMixin
Expand All @@ -24,7 +34,7 @@
from robot.running.librarykeywordrunner import LibraryKeywordRunner
from robot.running.model import Body
from robot.utils.error import ErrorDetails
from typing_extensions import override
from typing_extensions import Concatenate, override

from pytest_robotframework import (
_get_status_reporter_failures, # pyright:ignore[reportPrivateUsage]
Expand Down Expand Up @@ -583,6 +593,19 @@ def wrapped(*args: P.args, **kwargs: P.kwargs) -> T:
return wrapped


_R = TypeVar("_R")


def _bound_method(instance: T, fn: Callable[Concatenate[T, P], _R]) -> Callable[P, _R]:
"""if the keyword we're patching is on a class library, we need to re-bound the method to the
instance"""

def inner(*args: P.args, **kwargs: P.kwargs) -> _R:
return fn(instance, *args, **kwargs)

return inner


# the methods used in this listener were added in robot 7. in robot 6 we do this by patching
# `LibraryKeywordRunner._runner_for` instead
if robot_6:
Expand All @@ -600,13 +623,20 @@ def _runner_for( # pyright:ignore[reportUnusedFunction] # noqa: PLR0917
named: dict[str, object],
) -> Function:
"""use the original function instead of the `@keyword` wrapped one"""
handler = _hide_already_raised_exception_from_robot_log(
cast(Function, getattr(handler, _keyword_original_function_attr, handler))
original_function: Function | None = getattr(handler, _keyword_original_function_attr, None)
wrapped_function = _hide_already_raised_exception_from_robot_log(
cast(
Function,
_bound_method(handler.__self__, original_function)
if original_function is not None and isinstance(handler, MethodType)
else (original_function or handler),
)
)
return old_method(self, context, handler, positional, named)
return old_method(self, context, wrapped_function, positional, named)

else:
from robot.running.librarykeyword import StaticKeyword
from robot.running.testlibraries import ClassLibrary

@catch_errors
class KeywordUnwrapper(ListenerV3):
Expand All @@ -623,14 +653,19 @@ def start_library_keyword(
):
if not isinstance(implementation, StaticKeyword):
return
unwrapped_method: Function | None = getattr(
original_function: Function | None = getattr(
implementation.method, _keyword_original_function_attr, None
)
if unwrapped_method is None:

if original_function is None:
return

setattr(
implementation.owner.instance, # pyright:ignore[reportAny]
implementation.method_name,
_hide_already_raised_exception_from_robot_log(unwrapped_method),
_hide_already_raised_exception_from_robot_log(
_bound_method(implementation.owner.instance, original_function) # pyright:ignore[reportAny]
if isinstance(implementation.owner, ClassLibrary)
else original_function
),
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# noqa: N999
# robot class libraries need to have the same name as the module
from __future__ import annotations

from robot.api import logger

from pytest_robotframework import keyword


class ClassLibrary:
def __init__(self):
pass

@keyword
def foo(self): # noqa: PLR6301
logger.info("hi")
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
*** Settings ***
Library ./ClassLibrary.py


*** Test Cases ***
Asdf
Foo
10 changes: 10 additions & 0 deletions tests/test_robot.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

from pytest import ExitCode, Item, Mark

from pytest_robotframework._internal.robot.utils import robot_6
from tests.conftest import PytestRobotTester, assert_robot_total_stats, output_xml, xpath

if TYPE_CHECKING:
Expand Down Expand Up @@ -326,3 +327,12 @@ def test_empty_setup_or_teardown(pr: PytestRobotTester):
)
assert not xml.xpath("//test[@name='Disable setup']/kw[@name='Setup']/kw[@name='Log']")
assert not xml.xpath("//test[@name='Disable teardown']/kw[@name='Teardown']/kw[@name='Log']")


def test_keyword_decorator_class_library(pr: PytestRobotTester):
pr.run_and_assert_result("--robot-loglevel", "DEBUG:INFO", passed=1)
pr.assert_log_file_exists()
assert xpath(
output_xml(),
f"//kw[@name='Foo' and @{'library' if robot_6 else 'owner'}='ClassLibrary']/msg[.='hi']",
)

0 comments on commit 4059a68

Please sign in to comment.