From 95647cdb0a401e4b5d85d833917873e26abb5558 Mon Sep 17 00:00:00 2001 From: Marcel Wilson Date: Thu, 11 Jan 2024 19:03:46 -0600 Subject: [PATCH] enabling ruff FBT (flake8-boolean-trap) and addressing #44 --- pyproject.toml | 2 +- screenpy_selenium/actions/enter.py | 6 +++- screenpy_selenium/actions/hold_down.py | 8 ++++- screenpy_selenium/actions/release.py | 8 ++++- screenpy_selenium/common.py | 42 +++++++++++++++++++++++++ screenpy_selenium/questions/selected.py | 7 ++++- screenpy_selenium/questions/text.py | 7 ++++- tests/test_actions.py | 40 +++++++++++++++++++++++ tests/test_questions.py | 27 ++++++++++++++++ 9 files changed, 141 insertions(+), 6 deletions(-) create mode 100644 screenpy_selenium/common.py diff --git a/pyproject.toml b/pyproject.toml index 8ff926d..07f6a3d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -120,7 +120,7 @@ select = [ "ERA", # eradicate "F", # Pyflakes "FA", # flake8-future-annotations -# "FBT", # flake8-boolean-trap + "FBT", # flake8-boolean-trap "FIX", # flake8-fixme "FLY", # flynt "I", # isort diff --git a/screenpy_selenium/actions/enter.py b/screenpy_selenium/actions/enter.py index 6c58879..a65a872 100644 --- a/screenpy_selenium/actions/enter.py +++ b/screenpy_selenium/actions/enter.py @@ -8,6 +8,7 @@ from screenpy.pacing import aside, beat from selenium.common.exceptions import WebDriverException +from ..common import pos_args_deprecated from ..speech_tools import KEY_NAMES if TYPE_CHECKING: @@ -151,7 +152,10 @@ def add_to_chain( for key in self.following_keys: send_keys(key) - def __init__(self: SelfEnter, text: str, mask: bool = False) -> None: + @pos_args_deprecated("mask") + def __init__( + self: SelfEnter, text: str, mask: bool = False # noqa: FBT001, FBT002 + ) -> None: self.text = text self.target = None self.following_keys = [] diff --git a/screenpy_selenium/actions/hold_down.py b/screenpy_selenium/actions/hold_down.py index a61d571..636feeb 100644 --- a/screenpy_selenium/actions/hold_down.py +++ b/screenpy_selenium/actions/hold_down.py @@ -8,6 +8,7 @@ from screenpy.pacing import beat from selenium.webdriver.common.keys import Keys +from ..common import pos_args_deprecated from ..speech_tools import KEY_NAMES if TYPE_CHECKING: @@ -87,7 +88,12 @@ def add_to_chain( msg = "HoldDown must be told what to hold down." raise UnableToAct(msg) - def __init__(self: SelfHoldDown, key: str | None = None, lmb: bool = False) -> None: + @pos_args_deprecated("lmb") + def __init__( + self: SelfHoldDown, + key: str | None = None, + lmb: bool = False, # noqa: FBT001, FBT002 + ) -> None: self.key = key self.lmb = lmb self.target = None diff --git a/screenpy_selenium/actions/release.py b/screenpy_selenium/actions/release.py index 0127e80..ebc6d2b 100644 --- a/screenpy_selenium/actions/release.py +++ b/screenpy_selenium/actions/release.py @@ -8,6 +8,7 @@ from screenpy.pacing import beat from selenium.webdriver.common.keys import Keys +from ..common import pos_args_deprecated from ..speech_tools import KEY_NAMES if TYPE_CHECKING: @@ -74,7 +75,12 @@ def add_to_chain(self: SelfRelease, _: Actor, the_chain: ActionChains) -> None: msg = "Release must be told what to release." raise UnableToAct(msg) - def __init__(self: SelfRelease, key: str | None = None, lmb: bool = False) -> None: + @pos_args_deprecated("lmb") + def __init__( + self: SelfRelease, + key: str | None = None, + lmb: bool = False, # noqa: FBT001, FBT002 + ) -> None: self.key = key self.lmb = lmb self.description = "LEFT MOUSE BUTTON" if lmb else KEY_NAMES[key] diff --git a/screenpy_selenium/common.py b/screenpy_selenium/common.py new file mode 100644 index 0000000..02b9eb1 --- /dev/null +++ b/screenpy_selenium/common.py @@ -0,0 +1,42 @@ +"""module to hold shared objects.""" +from __future__ import annotations + +import warnings +from functools import wraps +from typing import TYPE_CHECKING, Callable, ParamSpec, TypeVar + +if TYPE_CHECKING: + P = ParamSpec("P") + T = TypeVar("T") + Function = Callable[P, T] + + +def pos_args_deprecated(*keywords: str) -> Function: + """Warn users which positional arguments should be called via keyword.""" + + def deprecated(func: Function) -> Function: + argnames = func.__code__.co_varnames[: func.__code__.co_argcount] + i = min([argnames.index(kw) for kw in keywords]) + kw_argnames = argnames[i:] + + @wraps(func) + def wrapper(*args: P.args, **kwargs: P.kwargs) -> Function: + # call the function first, to make sure the signature matches + ret_value = func(*args, **kwargs) + + args_that_should_be_kw = args[i:] + if args_that_should_be_kw: + posargnames = ", ".join(kw_argnames) + + msg = ( + f"Warning: positional arguments `{posargnames}` for " + f"`{func.__qualname__}` are deprecated. " + f"Please use keyword arguments instead." + ) + warnings.warn(msg, DeprecationWarning, stacklevel=2) + + return ret_value + + return wrapper + + return deprecated diff --git a/screenpy_selenium/questions/selected.py b/screenpy_selenium/questions/selected.py index fd89dfb..a458ad0 100644 --- a/screenpy_selenium/questions/selected.py +++ b/screenpy_selenium/questions/selected.py @@ -6,6 +6,8 @@ from screenpy.pacing import beat from selenium.webdriver.support.ui import Select as SeleniumSelect +from ..common import pos_args_deprecated + if TYPE_CHECKING: from screenpy import Actor @@ -89,6 +91,9 @@ def answered_by(self: SelfSelected, the_actor: Actor) -> str | list[str]: return [e.text for e in select.all_selected_options] return select.first_selected_option.text - def __init__(self: SelfSelected, target: Target, multi: bool = False) -> None: + @pos_args_deprecated("multi") + def __init__( + self: SelfSelected, target: Target, multi: bool = False # noqa: FBT001, FBT002 + ) -> None: self.target = target self.multi = multi diff --git a/screenpy_selenium/questions/text.py b/screenpy_selenium/questions/text.py index 685b08f..807bd06 100644 --- a/screenpy_selenium/questions/text.py +++ b/screenpy_selenium/questions/text.py @@ -5,6 +5,8 @@ from screenpy.pacing import beat +from ..common import pos_args_deprecated + if TYPE_CHECKING: from screenpy import Actor @@ -69,6 +71,9 @@ def answered_by(self: SelfText, the_actor: Actor) -> str | list[str]: return [e.text for e in self.target.all_found_by(the_actor)] return self.target.found_by(the_actor).text - def __init__(self: SelfText, target: Target, multi: bool = False) -> None: + @pos_args_deprecated("multi") + def __init__( + self: SelfText, target: Target, multi: bool = False # noqa: FBT001, FBT002 + ) -> None: self.target = target self.multi = multi diff --git a/tests/test_actions.py b/tests/test_actions.py index 743a4f4..dada236 100644 --- a/tests/test_actions.py +++ b/tests/test_actions.py @@ -1,5 +1,6 @@ from __future__ import annotations +import warnings from typing import cast from unittest import mock @@ -435,6 +436,19 @@ def new_method(self) -> bool: assert SubEnter.the_text("blah").new_method() is True + def test_positional_arg_warns(self) -> None: + with pytest.warns(DeprecationWarning): + Enter("", True) + + def test_keyword_arg_does_not_warn(self) -> None: + with warnings.catch_warnings(): + warnings.simplefilter("error") + Enter.the_secret("") + + with warnings.catch_warnings(): + warnings.simplefilter("error") + Enter("", mask=True) + class TestEnter2FAToken: def test_can_be_instantiated(self) -> None: @@ -619,6 +633,19 @@ def new_method(self) -> bool: assert SubHoldDown.left_mouse_button().new_method() is True + def test_positional_arg_warns(self) -> None: + with pytest.warns(DeprecationWarning): + HoldDown(Keys.LEFT_ALT, True) + + def test_keyword_arg_does_not_warn(self) -> None: + with warnings.catch_warnings(): + warnings.simplefilter("error") + HoldDown.left_mouse_button() + + with warnings.catch_warnings(): + warnings.simplefilter("error") + HoldDown(lmb=True) + class TestMoveMouse: def test_can_be_instantiated(self) -> None: @@ -884,6 +911,19 @@ def new_method(self) -> bool: assert SubRelease.left_mouse_button().new_method() is True + def test_positional_arg_warns(self) -> None: + with pytest.warns(DeprecationWarning): + Release(Keys.LEFT_ALT, True) + + def test_keyword_arg_does_not_warn(self) -> None: + with warnings.catch_warnings(): + warnings.simplefilter("error") + Release.left_mouse_button() + + with warnings.catch_warnings(): + warnings.simplefilter("error") + Release(lmb=True) + class TestRespondToThePrompt: def test_can_be_instantiated(self) -> None: diff --git a/tests/test_questions.py b/tests/test_questions.py index 6517afd..23dbd1b 100644 --- a/tests/test_questions.py +++ b/tests/test_questions.py @@ -1,5 +1,6 @@ from __future__ import annotations +import warnings from unittest import mock import pytest @@ -309,6 +310,19 @@ def test_describe(self) -> None: Selected(TARGET).describe() == f"The selected option(s) from the {TARGET}." ) + def test_positional_arg_warns(self) -> None: + with pytest.warns(DeprecationWarning): + Selected(TARGET, True) + + def test_keyword_arg_does_not_warn(self) -> None: + with warnings.catch_warnings(): + warnings.simplefilter("error") + Selected.options_from_the(TARGET) + + with warnings.catch_warnings(): + warnings.simplefilter("error") + Selected(TARGET, multi=True) + class TestText: def test_can_be_instantiated(self) -> None: @@ -379,3 +393,16 @@ def test_ask_for_text_of_the_alert(self, Tester: Actor) -> None: def test_describe(self) -> None: assert TextOfTheAlert().describe() == "The text of the alert." + + def test_positional_arg_warns(self) -> None: + with pytest.warns(DeprecationWarning): + Text(TARGET, True) + + def test_keyword_arg_does_not_warn(self) -> None: + with warnings.catch_warnings(): + warnings.simplefilter("error") + Text.of_all(TARGET) + + with warnings.catch_warnings(): + warnings.simplefilter("error") + Text(TARGET, multi=True)