From 09209d506a44d8da3edf2224203b5794cef50098 Mon Sep 17 00:00:00 2001 From: detachhead Date: Wed, 13 Sep 2023 09:53:26 +1000 Subject: [PATCH] downgrade code to work with 3.8 --- .github/workflows/check and publish.yaml | 2 +- poetry.lock | 2 +- pyproject.toml | 1 + pytest_robotframework/__init__.py | 50 ++++++++++--------- pytest_robotframework/_internal/plugin.py | 39 +++++++++------ .../_internal/robot_classes.py | 25 ++++------ .../_internal/robot_library.py | 7 +-- .../_internal/robot_utils.py | 4 +- pytest_robotframework/_internal/utils.py | 3 +- ...or_context_manager_that_doesnt_suppress.py | 6 +-- ..._keyword_decorator_custom_name_and_tags.py | 6 +-- .../foo.py | 5 +- tests/utils.py | 6 +-- 13 files changed, 83 insertions(+), 73 deletions(-) diff --git a/.github/workflows/check and publish.yaml b/.github/workflows/check and publish.yaml index 363496dd..88db4be7 100644 --- a/.github/workflows/check and publish.yaml +++ b/.github/workflows/check and publish.yaml @@ -34,7 +34,7 @@ jobs: - run: poetry lock --check - run: poetry install - run: poetry run mypy -p pytest_robotframework --python-version ${{ matrix.python_version }} - - run: poetry run mypy tests --python-version 3.11 + - run: poetry run mypy tests --python-version ${{ matrix.python_version }} - run: poetry run black --check --diff . - run: poetry run ruff . - run: poetry run pylint pytest_robotframework tests diff --git a/poetry.lock b/poetry.lock index d77d3de1..73342dd9 100644 --- a/poetry.lock +++ b/poetry.lock @@ -914,4 +914,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.8" -content-hash = "2b4f44b668bbf55d69a0e667457f0707bb68727f1388f6af9b0630545995833b" +content-hash = "6b3923e050597865cbaab095c66587e29a02051825c301bf4e444d025ecc1b9e" diff --git a/pyproject.toml b/pyproject.toml index 91615be9..ef65c461 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,6 +12,7 @@ pytest = "^7" robotframework = "^6.1.1" deepmerge = "^1.1.0" basedtyping = ">=0.0.3 <0.2" +exceptiongroup = "^1.1.3" [tool.poetry.group.dev.dependencies] black = ">=23" diff --git a/pytest_robotframework/__init__.py b/pytest_robotframework/__init__.py index a5e65e8c..50f39ed9 100644 --- a/pytest_robotframework/__init__.py +++ b/pytest_robotframework/__init__.py @@ -3,18 +3,17 @@ from __future__ import annotations import inspect -from collections import defaultdict from contextlib import AbstractContextManager, nullcontext from functools import wraps from pathlib import Path - -# yes lets put the Callable type in the collection module.... because THAT makes sense!!! -# said no one ever -from typing import ( # noqa: UP035 +from typing import ( TYPE_CHECKING, Callable, - ParamSpec, + DefaultDict, + Dict, + Type, TypeVar, + Union, cast, overload, ) @@ -27,7 +26,7 @@ from robot.running.librarykeywordrunner import LibraryKeywordRunner from robot.running.statusreporter import StatusReporter from robot.utils import getshortdoc -from typing_extensions import override +from typing_extensions import ParamSpec, override from pytest_robotframework._internal.errors import UserError from pytest_robotframework._internal.robot_utils import execution_context @@ -39,12 +38,12 @@ from robot.running.context import _ExecutionContext -RobotVariables = dict[str, object] +RobotVariables = Dict[str, object] -Listener = ListenerV2 | ListenerV3 +Listener = Union[ListenerV2, ListenerV3] -_suite_variables = defaultdict[Path, RobotVariables](dict) +_suite_variables = DefaultDict[Path, RobotVariables](dict) def set_variables(variables: RobotVariables): @@ -56,7 +55,7 @@ def set_variables(variables: RobotVariables): _suite_variables[suite_path] = variables -_resources = list[Path]() +_resources: list[Path] = [] def import_resource(path: Path | str): @@ -163,21 +162,24 @@ def exit_status_reporter( else: status_reporter.__exit__(type(error), error, error.__traceback__) - class WrappedContextManager(AbstractContextManager[T]): + # https://github.com/python/mypy/issues/16097 + class WrappedContextManager(AbstractContextManager): # type:ignore[type-arg] """defers exiting the status reporter until after the wrapped context manager is finished""" def __init__( self, - wrapped: AbstractContextManager[T], - status_reporter: AbstractContextManager[None], + wrapped: AbstractContextManager, # type:ignore[type-arg] + status_reporter: AbstractContextManager, # type:ignore[type-arg] ) -> None: - self.wrapped = wrapped - self.status_reporter = status_reporter + self.wrapped = wrapped # type:ignore[no-any-expr] + self.status_reporter = status_reporter # type:ignore[no-any-expr] @override def __enter__(self) -> T: - return self.wrapped.__enter__() + return ( # type:ignore[no-any-return] + self.wrapped.__enter__() # type:ignore[no-any-expr,no-untyped-call] + ) @override def __exit__( @@ -187,8 +189,10 @@ def __exit__( traceback: TracebackType | None, /, ) -> bool | None: - suppress = self.wrapped.__exit__(exc_type, exc_value, traceback) - exit_status_reporter(self.status_reporter) + suppress = self.wrapped.__exit__( # type:ignore[no-any-expr] + exc_type, exc_value, traceback + ) + exit_status_reporter(self.status_reporter) # type:ignore[no-any-expr] return suppress @wraps(fn) @@ -290,10 +294,10 @@ def keywordify( ) -_errors = list[Exception]() +_errors: list[Exception] = [] _T_ListenerOrSuiteVisitor = TypeVar( - "_T_ListenerOrSuiteVisitor", bound=type[Listener | SuiteVisitor] + "_T_ListenerOrSuiteVisitor", bound=Type[Union[Listener, SuiteVisitor]] ) @@ -336,13 +340,13 @@ def inner(*args: _P.args, **kwargs: _P.kwargs) -> T: class _ListenerRegistry: def __init__(self): - self.instances = list[Listener]() + self.instances: list[Listener] = [] self.too_late = False _listeners = _ListenerRegistry() -_T_Listener = TypeVar("_T_Listener", bound=type[Listener]) +_T_Listener = TypeVar("_T_Listener", bound=Type[Listener]) def listener(cls: _T_Listener) -> _T_Listener: diff --git a/pytest_robotframework/_internal/plugin.py b/pytest_robotframework/_internal/plugin.py index 4f59ec2c..e2a2a1b2 100644 --- a/pytest_robotframework/_internal/plugin.py +++ b/pytest_robotframework/_internal/plugin.py @@ -2,10 +2,11 @@ from __future__ import annotations -from typing import TYPE_CHECKING, cast +from typing import TYPE_CHECKING, Dict, cast import pytest from deepmerge import always_merger +from exceptiongroup import ExceptionGroup from pytest import TestReport from robot.api import logger from robot.libraries.BuiltIn import BuiltIn @@ -49,12 +50,12 @@ def _collect_slash_run(session: Session, *, collect_only: bool): if _listeners.too_late: raise InternalError("somehow ran collect/run twice???") robot = RobotFramework() # type:ignore[no-untyped-call] - robot_arg_list = list[str]() + robot_arg_list: list[str] = [] session.config.hook.pytest_robot_modify_args( args=robot_arg_list, session=session, collect_only=collect_only ) robot_args = cast( - dict[str, object], + Dict[str, object], always_merger.merge( # type:ignore[no-untyped-call] robot.parse_arguments( # type:ignore[no-untyped-call] [ # type:ignore[no-any-expr] @@ -63,27 +64,37 @@ def _collect_slash_run(session: Session, *, collect_only: bool): session.path, ] )[0], - dict[str, object]( - extension="py:robot", - runemptysuite=True, - parser=[PythonParser(session)], - prerunmodifier=[PytestCollector(session, collect_only=collect_only)], - ), + { # type:ignore[no-any-expr] + "extension": "py:robot", + "runemptysuite": True, + "parser": [PythonParser(session)], # type:ignore[no-any-expr] + "prerunmodifier": [ # type:ignore[no-any-expr] + PytestCollector(session, collect_only=collect_only) + ], + }, ), ) if collect_only: - robot_args |= {"report": None, "output": None, "log": None, "exitonerror": True} + robot_args = { + **robot_args, + "report": None, + "output": None, + "log": None, + "exitonerror": True, + } else: robot_args = always_merger.merge( # type:ignore[no-untyped-call] robot_args, - dict[str, object]( - prerunmodifier=[PytestRuntestProtocolInjector(session)], - listener=[ + { # type:ignore[no-any-expr] + "prerunmodifier": [ # type:ignore[no-any-expr] + PytestRuntestProtocolInjector(session) + ], + "listener": [ # type:ignore[no-any-expr] PytestRuntestProtocolHooks(session), ErrorDetector(session), *_listeners.instances, ], - ), + }, ) _listeners.too_late = True # needed for log_file listener methods to prevent logger from deactivating after the test is diff --git a/pytest_robotframework/_internal/robot_classes.py b/pytest_robotframework/_internal/robot_classes.py index 78a3d1b3..4f25b4ad 100644 --- a/pytest_robotframework/_internal/robot_classes.py +++ b/pytest_robotframework/_internal/robot_classes.py @@ -5,16 +5,7 @@ from contextlib import suppress from types import ModuleType - -# callable is not a collection -from typing import ( # noqa: UP035 - TYPE_CHECKING, - Callable, - Generator, - Literal, - ParamSpec, - cast, -) +from typing import TYPE_CHECKING, Callable, Generator, List, Literal, cast from _pytest import runner from pluggy import HookCaller, HookImpl @@ -23,7 +14,7 @@ from robot.api import SuiteVisitor from robot.api.interfaces import ListenerV3, Parser, TestDefaults from robot.running.model import Body -from typing_extensions import override +from typing_extensions import ParamSpec, override from pytest_robotframework import catch_errors from pytest_robotframework._internal import robot_library @@ -275,10 +266,10 @@ class PytestRuntestProtocolHooks(ListenerV3): def __init__(self, session: Session): self.session = session self.stop_running_hooks = False - self.hookwrappers = dict[HookImpl, _HookWrapper]() + self.hookwrappers: dict[HookImpl, _HookWrapper] = {} """hookwrappers that are in the middle of running""" - self.start_test_hooks = list[HookImpl]() - self.end_test_hooks = list[HookImpl]() + self.start_test_hooks: list[HookImpl] = [] + self.end_test_hooks: list[HookImpl] = [] def _get_item(self, data: running.TestCase) -> Item: item = _get_item_from_robot_test(self.session, data) @@ -416,7 +407,7 @@ def end_test( self._call_hooks(item, self.end_test_hooks) -robot_errors_key = StashKey[list[model.Message]]() +robot_errors_key = StashKey[List[model.Message]]() @catch_errors @@ -463,5 +454,7 @@ def log_message(self, message: model.Message): f" {self.current_test})" ) if not item.stash.get(robot_errors_key, None): - item.stash[robot_errors_key] = list[model.Message]() + # TODO: why can't mypy infer the type here? + # https://github.com/DetachHead/pytest-robotframework/issues/36 + item.stash[robot_errors_key] = [] # type:ignore[misc] item.stash[robot_errors_key].append(message) diff --git a/pytest_robotframework/_internal/robot_library.py b/pytest_robotframework/_internal/robot_library.py index 23506980..4d5f22a0 100644 --- a/pytest_robotframework/_internal/robot_library.py +++ b/pytest_robotframework/_internal/robot_library.py @@ -3,7 +3,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Literal, cast +from typing import TYPE_CHECKING, List, Literal, cast from _pytest._code.code import ExceptionInfo, ExceptionRepr from _pytest.runner import call_and_report, show_test_item @@ -16,7 +16,7 @@ if TYPE_CHECKING: from pytest_robotframework._internal.robot_utils import Cloaked -_report_key = StashKey[list[TestReport]]() +_report_key = StashKey[List[TestReport]]() def _call_and_report_robot_edition( @@ -24,10 +24,11 @@ def _call_and_report_robot_edition( ): """wrapper for the `call_and_report` function used by `_pytest.runner.runtestprotocol` with additional logic to show the result in the robot log""" + reports: list[TestReport] if _report_key in item.stash: reports = item.stash[_report_key] else: - reports = list[TestReport]() + reports = [] item.stash[_report_key] = reports report = call_and_report( # type:ignore[no-untyped-call] item, when, log=True, **kwargs diff --git a/pytest_robotframework/_internal/robot_utils.py b/pytest_robotframework/_internal/robot_utils.py index 58cd0fdd..b0c8f8fd 100644 --- a/pytest_robotframework/_internal/robot_utils.py +++ b/pytest_robotframework/_internal/robot_utils.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Generic, cast +from typing import Generic, Union, cast from basedtyping import T from robot.running.context import _ExecutionContext @@ -22,4 +22,4 @@ def execution_context() -> _ExecutionContext | None: # need to import it every time because it changes from robot.running import EXECUTION_CONTEXTS - return cast(_ExecutionContext | None, EXECUTION_CONTEXTS.current) + return cast(Union[_ExecutionContext, None], EXECUTION_CONTEXTS.current) diff --git a/pytest_robotframework/_internal/utils.py b/pytest_robotframework/_internal/utils.py index a356ee66..b5f4d7ed 100644 --- a/pytest_robotframework/_internal/utils.py +++ b/pytest_robotframework/_internal/utils.py @@ -1,9 +1,10 @@ from __future__ import annotations from functools import wraps -from typing import Callable, Concatenate, ParamSpec, cast # noqa: UP035 +from typing import Callable, cast from basedtyping import T +from typing_extensions import Concatenate, ParamSpec P = ParamSpec("P") diff --git a/tests/fixtures/test_python/test_keyword_decorator_context_manager_that_doesnt_suppress.py b/tests/fixtures/test_python/test_keyword_decorator_context_manager_that_doesnt_suppress.py index 6ad5fc22..683d5f4f 100644 --- a/tests/fixtures/test_python/test_keyword_decorator_context_manager_that_doesnt_suppress.py +++ b/tests/fixtures/test_python/test_keyword_decorator_context_manager_that_doesnt_suppress.py @@ -1,9 +1,7 @@ from __future__ import annotations from contextlib import AbstractContextManager, contextmanager - -# callable isnt a collection -from typing import TYPE_CHECKING, Callable, assert_type # noqa: UP035 +from typing import TYPE_CHECKING, Callable from robot.api import logger @@ -12,6 +10,8 @@ if TYPE_CHECKING: from collections.abc import Iterator + from typing_extensions import assert_type + @keyword @contextmanager diff --git a/tests/fixtures/test_python/test_keyword_decorator_custom_name_and_tags.py b/tests/fixtures/test_python/test_keyword_decorator_custom_name_and_tags.py index c280e154..cd341fc9 100644 --- a/tests/fixtures/test_python/test_keyword_decorator_custom_name_and_tags.py +++ b/tests/fixtures/test_python/test_keyword_decorator_custom_name_and_tags.py @@ -1,15 +1,15 @@ from __future__ import annotations from contextlib import AbstractContextManager, contextmanager - -# Callable isnt a collection -from typing import TYPE_CHECKING, Callable, assert_type # noqa: UP035 +from typing import TYPE_CHECKING, Callable from pytest_robotframework import keyword if TYPE_CHECKING: from collections.abc import Iterator + from typing_extensions import assert_type + @keyword(name="foo bar", tags=("a", "b")) def foo(): ... diff --git a/tests/fixtures/test_robot/test_keyword_decorator_and_other_decorator/foo.py b/tests/fixtures/test_robot/test_keyword_decorator_and_other_decorator/foo.py index 141befd8..973ea97d 100644 --- a/tests/fixtures/test_robot/test_keyword_decorator_and_other_decorator/foo.py +++ b/tests/fixtures/test_robot/test_keyword_decorator_and_other_decorator/foo.py @@ -2,11 +2,10 @@ from __future__ import annotations from functools import wraps - -# callable isnt a collection -from typing import TYPE_CHECKING, Callable, ParamSpec # noqa: UP035 +from typing import TYPE_CHECKING, Callable from robot.api import logger +from typing_extensions import ParamSpec from pytest_robotframework import keyword diff --git a/tests/utils.py b/tests/utils.py index 0633ec31..cb6f1f22 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,13 +1,13 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Never, cast +from typing import TYPE_CHECKING, Dict, cast from lxml.etree import XML from pytest import ExitCode, Pytester if TYPE_CHECKING: from lxml.etree import _Element - from typing_extensions import override + from typing_extensions import Never, override if TYPE_CHECKING: @@ -44,7 +44,7 @@ def assert_robot_total_stats(pytester: Pytester, *, passed=0, skipped=0, failed= statistics = next(child for child in root if child.tag == "statistics") total = next(child for child in statistics if child.tag == "total") result = cast( - dict[str, str], + Dict[str, str], next(child for child in total if child.tag == "stat").attrib.__copy__(), ) assert result == {"pass": str(passed), "fail": str(failed), "skip": str(skipped)}