From 683e18d4f70e9dc8bf9860953e5bb2b0074c0f7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niko=20F=C3=B6hr?= Date: Sun, 14 Apr 2024 21:46:49 +0300 Subject: [PATCH 1/8] replace create_mode with Mode.from_name --- src/wakepy/__main__.py | 4 +- src/wakepy/core/mode.py | 121 +++++++++++++++++++----------------- src/wakepy/core/registry.py | 4 +- src/wakepy/modes/keep.py | 10 +-- 4 files changed, 73 insertions(+), 66 deletions(-) diff --git a/src/wakepy/__main__.py b/src/wakepy/__main__.py index 43cb92bd..19f56ef1 100644 --- a/src/wakepy/__main__.py +++ b/src/wakepy/__main__.py @@ -20,7 +20,7 @@ from wakepy import ModeExit from wakepy.core.constants import ModeName -from wakepy.core.mode import create_mode +from wakepy.core.mode import Mode if typing.TYPE_CHECKING: from typing import List @@ -44,7 +44,7 @@ def main() -> None: modename = parse_arguments(sys.argv[1:]) - mode = create_mode(modename=modename, on_fail=handle_activation_error) + mode = Mode.from_name(modename, on_fail=handle_activation_error) print(get_startup_text(mode=modename)) with mode: if not mode.active: diff --git a/src/wakepy/core/mode.py b/src/wakepy/core/mode.py index 99578d88..f2d01caf 100644 --- a/src/wakepy/core/mode.py +++ b/src/wakepy/core/mode.py @@ -267,65 +267,72 @@ def __exit__( return False + @classmethod + def from_name( + cls, + modename: ModeName, + methods: Optional[StrCollection] = None, + omit: Optional[StrCollection] = None, + methods_priority: Optional[MethodsPriorityOrder] = None, + on_fail: OnFail = "error", + dbus_adapter: Type[DBusAdapter] | DBusAdapterTypeSeq | None = None, + ) -> Mode: + """ + Creates and returns a Mode based on a mode name. + + Parameters + ---------- + modename: + The name of the mode to create. Used for debugging, logging, + warning and error messages. Could be basically any string. + methods: list, tuple or set of str + The names of Methods to select from the mode defined with + `modename`; a "whitelist" filter. Means "use these and only these + Methods". Any Methods in `methods` but not in the selected mode + will raise a ValueError. Cannot be used same time with `omit`. + Optional. + omit: list, tuple or set of str or None + The names of Methods to remove from the mode defined with + `modename`; a "blacklist" filter. Any Method in `omit` but not in + the selected mode will be silently ignored. Cannot be used same + time with `methods`. Optional. + on_fail: "error" | "warn" | "pass" | Callable + Determines what to do in case mode activation fails. Valid options + are: "error", "warn", "pass" and a callable. If the option is + "error", raises wakepy.ActivationError. Is selected "warn", issues + warning. If "pass", does nothing. If `on_fail` is a callable, it + must take one positional argument: result, which is an instance of + ActivationResult. The ActivationResult contains more detailed + information about the activation process. + methods_priority: list[str | set[str]] + The priority order, which is a list of method names or asterisk + ('*'). The asterisk means "all rest methods" and may occur only + once in the priority order, and cannot be part of a set. All method + names must be unique and must be part of the `methods`. + dbus_adapter: + For using a custom dbus-adapter. Optional. If not given, the + default dbus adapter is used, which is :class:`~wakepy.\\ + dbus_adapters.jeepney.JeepneyDBusAdapter` -def create_mode( - modename: ModeName, - methods: Optional[StrCollection] = None, - omit: Optional[StrCollection] = None, - methods_priority: Optional[MethodsPriorityOrder] = None, - on_fail: OnFail = "error", - dbus_adapter: Type[DBusAdapter] | DBusAdapterTypeSeq | None = None, -) -> Mode: - """ - Creates and returns a Mode (a context manager). - - Parameters - ---------- - modename: - The name of the mode to create. Used for debugging, logging, warning - and error messaged. Could be basically any string. - methods: list, tuple or set of str - The names of Methods to select from the mode defined with `modename`; - a "whitelist" filter. Means "use these and only these Methods". Any - Methods in `methods` but not in the selected mode will raise a - ValueError. Cannot be used same time with `omit`. Optional. - omit: list, tuple or set of str or None - The names of Methods to remove from the mode defined with `modename`; - a "blacklist" filter. Any Method in `omit` but not in the selected mode - will be silently ignored. Cannot be used same time with `methods`. - Optional. - on_fail: "error" | "warn" | "pass" | Callable - Determines what to do in case mode activation fails. Valid options - are: "error", "warn", "pass" and a callable. If the option is - "error", raises wakepy.ActivationError. Is selected "warn", issues - warning. If "pass", does nothing. If `on_fail` is a callable, it - must take one positional argument: result, which is an instance of - ActivationResult. The ActivationResult contains more detailed - information about the activation process. - methods_priority: list[str | set[str]] - The priority order, which is a list of method names or asterisk - ('*'). The asterisk means "all rest methods" and may occur only - once in the priority order, and cannot be part of a set. All method - names must be unique and must be part of the `methods`. - dbus_adapter: - For using a custom dbus-adapter. Optional. If not given, the - default dbus adapter is used, which is :class:`~wakepy.dbus_adapters.\\ - jeepney.JeepneyDBusAdapter` + Returns + ------- + mode: Mode + The context manager for the selected mode. - Returns - ------- - mode: Mode - The context manager for the selected mode. - """ - methods_for_mode = get_methods_for_mode(modename) - selected_methods = select_methods(methods_for_mode, use_only=methods, omit=omit) - return Mode( - name=modename, - methods=selected_methods, - methods_priority=methods_priority, - on_fail=on_fail, - dbus_adapter=dbus_adapter, - ) + Raises + ------ + ValueError, if the mode name is not associated with any registered + (imported) Methods. + """ + methods_for_mode = get_methods_for_mode(modename) + selected_methods = select_methods(methods_for_mode, use_only=methods, omit=omit) + return cls( + name=modename, + methods=selected_methods, + methods_priority=methods_priority, + on_fail=on_fail, + dbus_adapter=dbus_adapter, + ) def handle_activation_fail(on_fail: OnFail, result: ActivationResult) -> None: diff --git a/src/wakepy/core/registry.py b/src/wakepy/core/registry.py index efdd8317..1d9f71fc 100644 --- a/src/wakepy/core/registry.py +++ b/src/wakepy/core/registry.py @@ -188,8 +188,8 @@ def get_methods( def get_methods_for_mode( mode: ModeName | str, ) -> List[MethodCls]: - """Get the Method classes belonging to a Mode; Methods with - Method.mode = `mode`. + """Get the Method classes belonging to a Mode; Methods with Method.mode = + `mode`. Parameters ---------- diff --git a/src/wakepy/modes/keep.py b/src/wakepy/modes/keep.py index 365f0c21..03c6d602 100644 --- a/src/wakepy/modes/keep.py +++ b/src/wakepy/modes/keep.py @@ -3,7 +3,7 @@ import typing from ..core.constants import ModeName -from ..core.mode import create_mode +from ..core.mode import Mode if typing.TYPE_CHECKING: from typing import Optional, Type @@ -77,8 +77,8 @@ def running( >>> with keep.running() as k: >>> # do something that takes a long time. """ - return create_mode( - modename=ModeName.KEEP_RUNNING, + return Mode.from_name( + ModeName.KEEP_RUNNING, omit=omit, methods=methods, methods_priority=methods_priority, @@ -151,8 +151,8 @@ def presenting( >>> # do something that takes a long time. """ - return create_mode( - modename=ModeName.KEEP_PRESENTING, + return Mode.from_name( + ModeName.KEEP_PRESENTING, methods=methods, omit=omit, methods_priority=methods_priority, From d6ed65f37520928185d28f8ee0b7bb9cdae9f90a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niko=20F=C3=B6hr?= Date: Sun, 14 Apr 2024 22:28:00 +0300 Subject: [PATCH 2/8] split handle_activation_error Easier to test if (1) forming the string is in one separate function and (2) the print is only called once --- src/wakepy/__main__.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/wakepy/__main__.py b/src/wakepy/__main__.py index 19f56ef1..088363ac 100644 --- a/src/wakepy/__main__.py +++ b/src/wakepy/__main__.py @@ -57,6 +57,10 @@ def main() -> None: def handle_activation_error(result: ActivationResult) -> None: + print(_get_activation_error_text(result)) + + +def _get_activation_error_text(result: ActivationResult) -> str: from wakepy import __version__ error_text = f""" @@ -74,8 +78,12 @@ def handle_activation_error(result: ActivationResult) -> None: Thank you! """ # noqa 501 + + out = [] for block in dedent(error_text.strip("\n")).split("\n"): - print(fill(block, 80)) + out.append(fill(block, 80)) + + return "\n".join(out) def parse_arguments( From 0618ac28983c2f2b528c9fcce35b074029d8fe7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niko=20F=C3=B6hr?= Date: Sun, 14 Apr 2024 22:32:27 +0300 Subject: [PATCH 3/8] fix version string on dev versions the version is of format: v.0.8.0.dev3+gf4b7249, so it takes more than 20 characters of space --- src/wakepy/__main__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/wakepy/__main__.py b/src/wakepy/__main__.py index 088363ac..32a6fdcd 100644 --- a/src/wakepy/__main__.py +++ b/src/wakepy/__main__.py @@ -33,7 +33,7 @@ \ \ /\ / // _` || |/ // _ \| '_ \ | | | | \ V V /| (_| || <| __/| |_) || |_| | \_/\_/ \__,_||_|\_\\___|| .__/ \__, | -{VERSION_STRING} | | __/ | +{VERSION_STRING}| | __/ | |_| |___/ """ WAKEPY_TICKBOXES_TEMPLATE = """ @@ -151,7 +151,7 @@ def get_startup_text(mode: ModeName) -> str: from wakepy import __version__ wakepy_text = WAKEPY_TEXT_TEMPLATE.format( - VERSION_STRING=f"{' v.'+__version__: <20}" + VERSION_STRING=f"{' v.'+__version__: <28}" ) options_txt = WAKEPY_TICKBOXES_TEMPLATE.strip("\n").format( no_auto_suspend="x", From b95a71e274f3a9235439bd06e7d54acf5d50365d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niko=20F=C3=B6hr?= Date: Sun, 14 Apr 2024 22:45:08 +0300 Subject: [PATCH 4/8] added logging for registering methods --- src/wakepy/core/registry.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/wakepy/core/registry.py b/src/wakepy/core/registry.py index 1d9f71fc..5eb6466e 100644 --- a/src/wakepy/core/registry.py +++ b/src/wakepy/core/registry.py @@ -16,6 +16,7 @@ from __future__ import annotations +import logging import typing from typing import overload @@ -47,6 +48,7 @@ """A name -> Method class mapping. Updated automatically; when python loads a module with a subclass of Method, the Method class is added to this registry. """ +logger = logging.getLogger(__name__) class MethodRegistryError(RuntimeError): @@ -58,8 +60,13 @@ def register_method(method_class: Type[Method]) -> None: if method_class.is_unnamed(): # Methods without a name will not be registered + logging.debug( + "Not registering Method %s as it does not have a name set.", method_class + ) return + logging.debug("Registering Method %s (name: %s)", method_class, method_class.name) + method_dict: MethodDict = _method_registry.get(method_class.mode, dict()) if method_class.name in method_dict: From 88eaf344eba354a0f88c180ebbadbcb1945b8ffb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niko=20F=C3=B6hr?= Date: Sun, 14 Apr 2024 22:54:14 +0300 Subject: [PATCH 5/8] rewrite tests for __main__: simplify --- tests/integration/test_main.py | 122 ++++++++++++++++++++++++++++++++ tests/unit/test_main.py | 123 +-------------------------------- 2 files changed, 125 insertions(+), 120 deletions(-) create mode 100644 tests/integration/test_main.py diff --git a/tests/integration/test_main.py b/tests/integration/test_main.py new file mode 100644 index 00000000..8c5457bc --- /dev/null +++ b/tests/integration/test_main.py @@ -0,0 +1,122 @@ +"""Integration tests for the __main__ CLI""" + +from unittest.mock import Mock, call, patch + +import pytest + +from wakepy.__main__ import _get_activation_error_text, get_startup_text, main +from wakepy.core import CURRENT_PLATFORM, ActivationResult, Method + + +@pytest.fixture +def modename_working(): + return "testmode_working" + + +@pytest.fixture +def modename_broken(): + return "testmode_broken" + + +@pytest.fixture +def method1(modename_working): + class WorkingMethod(Method): + """This is a succesful method as it implements enter_mode which returns + None""" + + name = "method1" + mode = modename_working + supported_platforms = (CURRENT_PLATFORM,) + + def enter_mode(self) -> None: + return + + return WorkingMethod + + +@pytest.fixture +def method2_broken(modename_broken): + class BrokenMethod(Method): + """This is a unsuccesful method as it implements enter_mode which + raises an Exception""" + + name = "method2_broken" + mode = modename_broken + supported_platforms = (CURRENT_PLATFORM,) + + def enter_mode(self) -> None: + raise RuntimeError("foo") + + return BrokenMethod + + +@patch("wakepy.__main__.wait_until_keyboardinterrupt") +@patch("wakepy.__main__.parse_arguments") +class TestMain: + """Tests the main() function from the __main__.py in a simple way. This + is more of a smoke test. The functionality of the different parts is + already tested in unit tests.""" + + def test_working_mode( + self, + parse_arguments, + wait_until_keyboardinterrupt, + method1, + ): + + with patch("sys.argv", self.sys_argv), patch("builtins.print") as print_mock: + manager = self.setup_mock_manager( + method1, print_mock, parse_arguments, wait_until_keyboardinterrupt + ) + main() + + assert manager.mock_calls == [ + call.print(get_startup_text(method1.mode)), + call.wait_until_keyboardinterrupt(), + call.print("\n\nExited."), + ] + + @pytest.mark.usefixtures("method2_broken") + def test_non_working_mode( + self, + parse_arguments, + wait_until_keyboardinterrupt, + method2_broken, + ): + + with patch("sys.argv", self.sys_argv), patch("builtins.print") as print_mock: + manager = self.setup_mock_manager( + method2_broken, + print_mock, + parse_arguments, + wait_until_keyboardinterrupt, + ) + main() + + expected_result = ActivationResult(results=[], modename=method2_broken.mode) + assert manager.mock_calls == [ + call.print(get_startup_text(method2_broken.mode)), + call.print(_get_activation_error_text(expected_result)), + ] + + @staticmethod + def setup_mock_manager( + method: Method, + print_mock, + parse_arguments, + wait_until_keyboardinterrupt, + ): + # Assume that user has specified some mode in the commandline which + # resolves to `method.mode` + parse_arguments.return_value = method.mode + + mocks = Mock() + mocks.attach_mock(print_mock, "print") + mocks.attach_mock(wait_until_keyboardinterrupt, "wait_until_keyboardinterrupt") + return mocks + + @property + def sys_argv(self): + # The patched value for sys.argv. Does not matter here otherwise, but + # should be a list of at least two items. + return ["", ""] diff --git a/tests/unit/test_main.py b/tests/unit/test_main.py index e83486ee..65ed819c 100644 --- a/tests/unit/test_main.py +++ b/tests/unit/test_main.py @@ -1,19 +1,17 @@ -"""Tests for the __main__ CLI""" +"""Unit tests for the __main__ module""" import sys -from unittest.mock import MagicMock, Mock, call, patch +from unittest.mock import patch import pytest -from wakepy import ActivationResult, ModeExit +from wakepy import ActivationResult from wakepy.__main__ import ( get_startup_text, handle_activation_error, - main, parse_arguments, wait_until_keyboardinterrupt, ) -from wakepy.core import Mode from wakepy.core.constants import ModeName @@ -69,121 +67,6 @@ def raise_keyboardinterrupt(_): wait_until_keyboardinterrupt() -@patch("wakepy.__main__.wait_until_keyboardinterrupt") -@patch("wakepy.__main__.get_startup_text") -@patch("wakepy.__main__.create_mode") -@patch("wakepy.__main__.parse_arguments") -class TestMain: - """Tests the main() function from the __main__.py""" - - def test_main( - self, - parse_arguments, - create_mode, - get_startup_text, - wait_until_keyboardinterrupt, - ): - """This is just a smoke test for the main() function. It checks that - correct functions are called in the correct order and correct - arguments, ut the functionality of each of the functions is tested - elsewhere.""" - - mocks = self.get_mocks_for_main( - parse_arguments, - create_mode, - get_startup_text, - wait_until_keyboardinterrupt, - mode_works=True, - ) - - with patch("sys.argv", mocks.sysarg), patch("builtins.print", mocks.print): - main() - - assert mocks.mock_calls == [ - call.parse_arguments(mocks.sysarg[1:]), - call.create_mode( - modename=parse_arguments.return_value, on_fail=handle_activation_error - ), - call.get_startup_text(mode=parse_arguments.return_value), - call.print(get_startup_text.return_value), - call.mode.__enter__(), - call.wait_until_keyboardinterrupt(), - call.mode.__exit__(None, None, None), - call.print("\n\nExited."), - ] - - def test_main_with_non_working_mode( - self, - parse_arguments, - create_mode, - get_startup_text, - wait_until_keyboardinterrupt, - ): - mocks = self.get_mocks_for_main( - parse_arguments, - create_mode, - get_startup_text, - wait_until_keyboardinterrupt, - mode_works=False, - ) - - with patch("sys.argv", mocks.sysarg), patch("builtins.print", mocks.print): - main() - - exit_call_args = mocks.mock_calls[-1][1] - assert mocks.mock_calls == [ - call.parse_arguments(mocks.sysarg[1:]), - call.create_mode( - modename=parse_arguments.return_value, on_fail=handle_activation_error - ), - call.get_startup_text(mode=parse_arguments.return_value), - call.print(get_startup_text.return_value), - call.mode.__enter__(), - # Checking only the exception type here. The exception and the - # traceback instances are assumed to be correct. Too complicated to - # catch them just for the test. - call.mode.__exit__(ModeExit, *exit_call_args[1:]), - ] - - @staticmethod - def get_mocks_for_main( - parse_arguments, - create_mode, - get_startup_text, - wait_until_keyboardinterrupt, - mode_works: bool, - ): - cli_arg = Mock() - mockmodename = Mock(spec_set=ModeName.KEEP_PRESENTING) - mockprint = Mock() - - class TestMode(Mode): - active = mode_works - activation_result: ActivationResult = ActivationResult() - - mockresult = MagicMock(spec_set=ActivationResult) - mockresult.success = mode_works - mockmode = MagicMock(spec_set=TestMode) - mockmode.__enter__.return_value = mockmode - mockmode.__exit__.return_value = True - mockmode.activation_result = mockresult - mockmode.active = mode_works - sysarg = ["programname", cli_arg] - parse_arguments.return_value = mockmodename - get_startup_text.return_value = "startuptext" - create_mode.return_value = mockmode - - mocks = Mock() - mocks.sysarg = sysarg - mocks.attach_mock(mockprint, "print") - mocks.attach_mock(mockmode, "mode") - mocks.attach_mock(parse_arguments, "parse_arguments") - mocks.attach_mock(create_mode, "create_mode") - mocks.attach_mock(get_startup_text, "get_startup_text") - mocks.attach_mock(wait_until_keyboardinterrupt, "wait_until_keyboardinterrupt") - return mocks - - @patch("builtins.print") def test_handle_activation_error(print_mock): result = ActivationResult() From a4c1ffedd7db6b900b9cbae7dfce4f4bd270ba39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niko=20F=C3=B6hr?= Date: Sun, 14 Apr 2024 23:18:58 +0300 Subject: [PATCH 6/8] move the test for main back to /unit tests --- tests/integration/test_main.py | 122 --------------------------------- tests/unit/test_main.py | 121 +++++++++++++++++++++++++++++++- 2 files changed, 119 insertions(+), 124 deletions(-) delete mode 100644 tests/integration/test_main.py diff --git a/tests/integration/test_main.py b/tests/integration/test_main.py deleted file mode 100644 index 8c5457bc..00000000 --- a/tests/integration/test_main.py +++ /dev/null @@ -1,122 +0,0 @@ -"""Integration tests for the __main__ CLI""" - -from unittest.mock import Mock, call, patch - -import pytest - -from wakepy.__main__ import _get_activation_error_text, get_startup_text, main -from wakepy.core import CURRENT_PLATFORM, ActivationResult, Method - - -@pytest.fixture -def modename_working(): - return "testmode_working" - - -@pytest.fixture -def modename_broken(): - return "testmode_broken" - - -@pytest.fixture -def method1(modename_working): - class WorkingMethod(Method): - """This is a succesful method as it implements enter_mode which returns - None""" - - name = "method1" - mode = modename_working - supported_platforms = (CURRENT_PLATFORM,) - - def enter_mode(self) -> None: - return - - return WorkingMethod - - -@pytest.fixture -def method2_broken(modename_broken): - class BrokenMethod(Method): - """This is a unsuccesful method as it implements enter_mode which - raises an Exception""" - - name = "method2_broken" - mode = modename_broken - supported_platforms = (CURRENT_PLATFORM,) - - def enter_mode(self) -> None: - raise RuntimeError("foo") - - return BrokenMethod - - -@patch("wakepy.__main__.wait_until_keyboardinterrupt") -@patch("wakepy.__main__.parse_arguments") -class TestMain: - """Tests the main() function from the __main__.py in a simple way. This - is more of a smoke test. The functionality of the different parts is - already tested in unit tests.""" - - def test_working_mode( - self, - parse_arguments, - wait_until_keyboardinterrupt, - method1, - ): - - with patch("sys.argv", self.sys_argv), patch("builtins.print") as print_mock: - manager = self.setup_mock_manager( - method1, print_mock, parse_arguments, wait_until_keyboardinterrupt - ) - main() - - assert manager.mock_calls == [ - call.print(get_startup_text(method1.mode)), - call.wait_until_keyboardinterrupt(), - call.print("\n\nExited."), - ] - - @pytest.mark.usefixtures("method2_broken") - def test_non_working_mode( - self, - parse_arguments, - wait_until_keyboardinterrupt, - method2_broken, - ): - - with patch("sys.argv", self.sys_argv), patch("builtins.print") as print_mock: - manager = self.setup_mock_manager( - method2_broken, - print_mock, - parse_arguments, - wait_until_keyboardinterrupt, - ) - main() - - expected_result = ActivationResult(results=[], modename=method2_broken.mode) - assert manager.mock_calls == [ - call.print(get_startup_text(method2_broken.mode)), - call.print(_get_activation_error_text(expected_result)), - ] - - @staticmethod - def setup_mock_manager( - method: Method, - print_mock, - parse_arguments, - wait_until_keyboardinterrupt, - ): - # Assume that user has specified some mode in the commandline which - # resolves to `method.mode` - parse_arguments.return_value = method.mode - - mocks = Mock() - mocks.attach_mock(print_mock, "print") - mocks.attach_mock(wait_until_keyboardinterrupt, "wait_until_keyboardinterrupt") - return mocks - - @property - def sys_argv(self): - # The patched value for sys.argv. Does not matter here otherwise, but - # should be a list of at least two items. - return ["", ""] diff --git a/tests/unit/test_main.py b/tests/unit/test_main.py index 65ed819c..6c9f72d7 100644 --- a/tests/unit/test_main.py +++ b/tests/unit/test_main.py @@ -1,20 +1,65 @@ """Unit tests for the __main__ module""" import sys -from unittest.mock import patch +from unittest.mock import Mock, call, patch import pytest -from wakepy import ActivationResult +from wakepy import ActivationResult, Method from wakepy.__main__ import ( + _get_activation_error_text, get_startup_text, handle_activation_error, + main, parse_arguments, wait_until_keyboardinterrupt, ) +from wakepy.core import CURRENT_PLATFORM from wakepy.core.constants import ModeName +@pytest.fixture +def modename_working(): + return "testmode_working" + + +@pytest.fixture +def modename_broken(): + return "testmode_broken" + + +@pytest.fixture +def method1(modename_working): + class WorkingMethod(Method): + """This is a succesful method as it implements enter_mode which returns + None""" + + name = "method1" + mode = modename_working + supported_platforms = (CURRENT_PLATFORM,) + + def enter_mode(self) -> None: + return + + return WorkingMethod + + +@pytest.fixture +def method2_broken(modename_broken): + class BrokenMethod(Method): + """This is a unsuccesful method as it implements enter_mode which + raises an Exception""" + + name = "method2_broken" + mode = modename_broken + supported_platforms = (CURRENT_PLATFORM,) + + def enter_mode(self) -> None: + raise RuntimeError("foo") + + return BrokenMethod + + @pytest.mark.parametrize( "args", [ @@ -78,3 +123,75 @@ def test_handle_activation_error(print_mock): printed_text = "\n".join(print_mock.mock_calls[0].args) # Some sensible text was printed to the user assert "Wakepy could not activate" in printed_text + + +@patch("wakepy.__main__.wait_until_keyboardinterrupt") +@patch("wakepy.__main__.parse_arguments") +class TestMain: + """Tests the main() function from the __main__.py in a simple way. This + is more of a smoke test. The functionality of the different parts is + already tested in other unit tests.""" + + def test_working_mode( + self, + parse_arguments, + wait_until_keyboardinterrupt, + method1, + ): + + with patch("sys.argv", self.sys_argv), patch("builtins.print") as print_mock: + manager = self.setup_mock_manager( + method1, print_mock, parse_arguments, wait_until_keyboardinterrupt + ) + main() + + assert manager.mock_calls == [ + call.print(get_startup_text(method1.mode)), + call.wait_until_keyboardinterrupt(), + call.print("\n\nExited."), + ] + + @pytest.mark.usefixtures("method2_broken") + def test_non_working_mode( + self, + parse_arguments, + wait_until_keyboardinterrupt, + method2_broken, + ): + + with patch("sys.argv", self.sys_argv), patch("builtins.print") as print_mock: + manager = self.setup_mock_manager( + method2_broken, + print_mock, + parse_arguments, + wait_until_keyboardinterrupt, + ) + main() + + expected_result = ActivationResult(results=[], modename=method2_broken.mode) + assert manager.mock_calls == [ + call.print(get_startup_text(method2_broken.mode)), + call.print(_get_activation_error_text(expected_result)), + ] + + @staticmethod + def setup_mock_manager( + method: Method, + print_mock, + parse_arguments, + wait_until_keyboardinterrupt, + ): + # Assume that user has specified some mode in the commandline which + # resolves to `method.mode` + parse_arguments.return_value = method.mode + + mocks = Mock() + mocks.attach_mock(print_mock, "print") + mocks.attach_mock(wait_until_keyboardinterrupt, "wait_until_keyboardinterrupt") + return mocks + + @property + def sys_argv(self): + # The patched value for sys.argv. Does not matter here otherwise, but + # should be a list of at least two items. + return ["", ""] From 82a604e02d01689a49bc53c19d1be7165f12b9bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niko=20F=C3=B6hr?= Date: Sun, 14 Apr 2024 23:25:16 +0300 Subject: [PATCH 7/8] Update from_name docstring --- src/wakepy/core/mode.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/wakepy/core/mode.py b/src/wakepy/core/mode.py index f2d01caf..29fc433b 100644 --- a/src/wakepy/core/mode.py +++ b/src/wakepy/core/mode.py @@ -319,10 +319,6 @@ def from_name( mode: Mode The context manager for the selected mode. - Raises - ------ - ValueError, if the mode name is not associated with any registered - (imported) Methods. """ methods_for_mode = get_methods_for_mode(modename) selected_methods = select_methods(methods_for_mode, use_only=methods, omit=omit) From b3174998fa6a773c6cb53bec98e3fd51bca11569 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niko=20F=C3=B6hr?= Date: Mon, 15 Apr 2024 00:11:06 +0300 Subject: [PATCH 8/8] set WAKEPY_FAKE_SUCCESS to false when testing __main__:main --- tests/unit/test_main.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/unit/test_main.py b/tests/unit/test_main.py index 6c9f72d7..fd18701f 100644 --- a/tests/unit/test_main.py +++ b/tests/unit/test_main.py @@ -157,7 +157,10 @@ def test_non_working_mode( parse_arguments, wait_until_keyboardinterrupt, method2_broken, + monkeypatch, ): + # need to turn off WAKEPY_FAKE_SUCCESS as we want to get a failure. + monkeypatch.setenv("WAKEPY_FAKE_SUCCESS", "0") with patch("sys.argv", self.sys_argv), patch("builtins.print") as print_mock: manager = self.setup_mock_manager(