From 675b245aa1462c30b2d91d5b88bf79ab38f707c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niko=20F=C3=B6hr?= Date: Thu, 14 Mar 2024 16:00:59 +0200 Subject: [PATCH 01/74] add __init__.py files to test folders --- tests/__init__.py | 0 tests/integration/__init__.py | 0 tests/integration/conftest.py | 2 +- tests/unit/__init__.py | 0 tests/unit/test_core/__init__.py | 0 tests/unit/test_core/test_activation/__init__.py | 0 tests/unit/test_core/test_activation/test_activation.py | 4 ++-- tests/unit/test_core/test_mode.py | 2 +- 8 files changed, 4 insertions(+), 4 deletions(-) create mode 100644 tests/__init__.py create mode 100644 tests/integration/__init__.py create mode 100644 tests/unit/__init__.py create mode 100644 tests/unit/test_core/__init__.py create mode 100644 tests/unit/test_core/test_activation/__init__.py diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/integration/__init__.py b/tests/integration/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index acf7aa45..ee27a6df 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -10,7 +10,7 @@ import pytest if sys.platform.lower().startswith("linux"): - from dbus_service import DBusService, start_dbus_service + from tests.integration.dbus_service import DBusService, start_dbus_service else: DBusService = None start_dbus_service = None diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/unit/test_core/__init__.py b/tests/unit/test_core/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/unit/test_core/test_activation/__init__.py b/tests/unit/test_core/test_activation/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/unit/test_core/test_activation/test_activation.py b/tests/unit/test_core/test_activation/test_activation.py index 75938fbb..abeac303 100644 --- a/tests/unit/test_core/test_activation/test_activation.py +++ b/tests/unit/test_core/test_activation/test_activation.py @@ -11,7 +11,8 @@ import pytest import time_machine -from testmethods import ( + +from tests.unit.test_core.testmethods import ( FAILURE_REASON, METHOD_MISSING, METHOD_OPTIONS, @@ -19,7 +20,6 @@ get_test_method_class, iterate_test_methods, ) - from wakepy.core import DBusAdapter, MethodActivationResult, get_methods from wakepy.core.activation import ( StageName, diff --git a/tests/unit/test_core/test_mode.py b/tests/unit/test_core/test_mode.py index ebb54b96..ee6edcd2 100644 --- a/tests/unit/test_core/test_mode.py +++ b/tests/unit/test_core/test_mode.py @@ -1,8 +1,8 @@ from unittest.mock import Mock, call import pytest -from testmethods import get_test_method_class +from tests.unit.test_core.testmethods import get_test_method_class from wakepy.core.dbus import DBusAdapter from wakepy.core.heartbeat import Heartbeat from wakepy.core.mode import ( From 1cdd2eced5453bdfccb1d8ff6a9595ed36ba555d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niko=20F=C3=B6hr?= Date: Thu, 14 Mar 2024 16:05:39 +0200 Subject: [PATCH 02/74] ignore venv with mypy --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 953dc7f5..1140c06e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -87,6 +87,7 @@ requires = ["flit_core >=3.2,<4"] build-backend = "flit_core.buildapi" [tool.mypy] +exclude = ['venv', '.venv'] check_untyped_defs = true disallow_any_generics = true no_implicit_optional = true From 2359143b25e86b88b177564abb8b8a45bb96c63e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niko=20F=C3=B6hr?= Date: Thu, 14 Mar 2024 16:09:10 +0200 Subject: [PATCH 03/74] always return a list in get_method Easier to work with the function when using mypy --- wakepy/core/registry.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/wakepy/core/registry.py b/wakepy/core/registry.py index 34d63fbe..df8b2302 100644 --- a/wakepy/core/registry.py +++ b/wakepy/core/registry.py @@ -127,7 +127,7 @@ def get_method(method_name: str, mode: Optional[ModeName] = None) -> MethodCls: def get_methods( names: Collection[str] | None = None, mode: Optional[ModeName] = None -) -> Collection[MethodCls] | None: +) -> Collection[MethodCls]: """Get a collection (list, tuple or set) of Method classes based on their names, and optionally the mode name. @@ -151,7 +151,7 @@ def get_methods( """ if names is None: - return None + return [] if isinstance(names, list): return [get_method(name, mode) for name in names] From 11dca4e3f1c8ef73b55b079868328045bf81a737 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niko=20F=C3=B6hr?= Date: Thu, 14 Mar 2024 16:09:31 +0200 Subject: [PATCH 04/74] add None check for p.stdout (for mypy) --- tests/integration/conftest.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index ee27a6df..b5e9311e 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -56,6 +56,9 @@ def private_bus(): env={"DBUS_VERBOSE": "1"}, ) + if p.stdout is None: + raise RuntimeError("Error when starting private bus") + bus_address = p.stdout.readline().decode("utf-8").strip() logger.info("Initiated private bus: %s", bus_address) From 6596b61012e98ebca7f23ec2248d051e1650d3c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niko=20F=C3=B6hr?= Date: Thu, 14 Mar 2024 17:07:35 +0200 Subject: [PATCH 05/74] fix a mypy warning in conftest.py about DbusService = None --- tests/integration/conftest.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index b5e9311e..175da63a 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -12,8 +12,11 @@ if sys.platform.lower().startswith("linux"): from tests.integration.dbus_service import DBusService, start_dbus_service else: - DBusService = None - start_dbus_service = None + + class DBusService: ... + + def start_dbus_service(): ... + from wakepy.core import DBusAddress, DBusMethod From 7257d57c3c32c8caaf5cd288ee43972e189897f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niko=20F=C3=B6hr?= Date: Thu, 14 Mar 2024 17:34:16 +0200 Subject: [PATCH 06/74] fix broken test for get_methods (was returning None, now []) --- tests/unit/test_core/test_registry.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unit/test_core/test_registry.py b/tests/unit/test_core/test_registry.py index c258b086..1181c9d9 100644 --- a/tests/unit/test_core/test_registry.py +++ b/tests/unit/test_core/test_registry.py @@ -64,8 +64,8 @@ class C(TestMethod): assert get_methods({"A", "B"}) == {A, B} assert get_methods({"C", "B"}) == {C, B} - # Asking None, getting None - assert get_methods(None) is None + # Asking None, getting empty list + assert get_methods(None) == [] # Asking something that does not exists will raise KeyError with pytest.raises(ValueError, match=re.escape('No Method with name "foo" found!')): From 066ba5c6566b54613ecbf161ad587bf25ee5d9ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niko=20F=C3=B6hr?= Date: Thu, 14 Mar 2024 17:34:39 +0200 Subject: [PATCH 07/74] fix mypy import warning --- .../unit/test_core/test_activation/test_activation.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tests/unit/test_core/test_activation/test_activation.py b/tests/unit/test_core/test_activation/test_activation.py index abeac303..eebc6c6e 100644 --- a/tests/unit/test_core/test_activation/test_activation.py +++ b/tests/unit/test_core/test_activation/test_activation.py @@ -20,7 +20,13 @@ get_test_method_class, iterate_test_methods, ) -from wakepy.core import DBusAdapter, MethodActivationResult, get_methods +from wakepy.core import ( + DBusAdapter, + Method, + MethodActivationResult, + PlatformName, + get_methods, +) from wakepy.core.activation import ( StageName, WakepyFakeSuccess, @@ -32,7 +38,7 @@ try_enter_and_heartbeat, ) from wakepy.core.heartbeat import Heartbeat -from wakepy.core.method import Method, MethodError, PlatformName +from wakepy.core.method import MethodError def test_activate_without_methods(monkeypatch): From 434ceeacf999637c847d833d2b745e305c358e97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niko=20F=C3=B6hr?= Date: Thu, 14 Mar 2024 18:43:00 +0200 Subject: [PATCH 08/74] fix equality checks between StrEnum and str (mypy) --- wakepy/core/strenum.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/wakepy/core/strenum.py b/wakepy/core/strenum.py index bf4410e2..6cb64543 100644 --- a/wakepy/core/strenum.py +++ b/wakepy/core/strenum.py @@ -113,7 +113,8 @@ class StrEnum(str, Enum, metaclass=ConstantEnumMeta): """ - def _generate_next_value_(name, *_): + @staticmethod + def _generate_next_value_(name: str, *_) -> str: """Turn auto() value to be a string corresponding to the enumeration member name @@ -136,6 +137,11 @@ def __new__(cls, val=None, *_): """ return EnumMemberString(val) + def __eq__(cls, other: object) -> bool: + if not isinstance(other, str): + return False + return str(cls) == other + __all__ = [ "StrEnum", From 7c529da5f1a881b388c8fb510c64535df2a1d29c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niko=20F=C3=B6hr?= Date: Thu, 14 Mar 2024 19:38:25 +0200 Subject: [PATCH 09/74] assert equality holds both ways with StrEnum and str --- tests/unit/test_core/test_strenum.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/unit/test_core/test_strenum.py b/tests/unit/test_core/test_strenum.py index 94f77de0..afa4af30 100644 --- a/tests/unit/test_core/test_strenum.py +++ b/tests/unit/test_core/test_strenum.py @@ -30,6 +30,8 @@ class MyConst(StrEnum): # Any auto() value is turned into a string which is same as # the enumeration member name assert MyConst.BAR == "BAR" + assert "BAR" == MyConst.BAR + assert isinstance(MyConst.BAR, str) From 7658bb4beebfed756766bf40ef9a6f2df28336b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niko=20F=C3=B6hr?= Date: Thu, 14 Mar 2024 19:52:02 +0200 Subject: [PATCH 10/74] define the __eq__ and __hash__ for the EnumMemberString Needed for == operation with StrEnum members --- wakepy/core/strenum.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/wakepy/core/strenum.py b/wakepy/core/strenum.py index 6cb64543..305f75b1 100644 --- a/wakepy/core/strenum.py +++ b/wakepy/core/strenum.py @@ -73,6 +73,12 @@ class EnumMemberString(str): it. """ + def __eq__(self, other: object) -> bool: + return str(self) == other + + def __hash__(self) -> int: + return super().__hash__() + class StrEnum(str, Enum, metaclass=ConstantEnumMeta): """A string constant / enumeration. For creating reusable, typed constants. @@ -137,11 +143,6 @@ def __new__(cls, val=None, *_): """ return EnumMemberString(val) - def __eq__(cls, other: object) -> bool: - if not isinstance(other, str): - return False - return str(cls) == other - __all__ = [ "StrEnum", From f5180c4a18ba9520e11632ef83a23b90ae4e0f70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niko=20F=C3=B6hr?= Date: Thu, 14 Mar 2024 19:59:55 +0200 Subject: [PATCH 11/74] mypy SomeConst.FOO == 'somestr' fix --- wakepy/core/strenum.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/wakepy/core/strenum.py b/wakepy/core/strenum.py index 305f75b1..fa673863 100644 --- a/wakepy/core/strenum.py +++ b/wakepy/core/strenum.py @@ -143,6 +143,12 @@ def __new__(cls, val=None, *_): """ return EnumMemberString(val) + def __eq__(self, other: object) -> bool: + # This was added just to make mypy happy. Without this mypy will + # assume SomeConst.FOO == 'somestr' always to be False. + # In reality, the EnumMemberString.__eq__ is called in this case. + return str(self) == other # pragma: no cover + __all__ = [ "StrEnum", From 7b14ddb6fbd2b7ce912d6dbfcc43c57e654f68d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niko=20F=C3=B6hr?= Date: Sun, 17 Mar 2024 17:11:17 +0200 Subject: [PATCH 12/74] remove unused EnumMemberString methods The __eq__ and __hash__ were not required after all. --- wakepy/core/strenum.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/wakepy/core/strenum.py b/wakepy/core/strenum.py index fa673863..436c4897 100644 --- a/wakepy/core/strenum.py +++ b/wakepy/core/strenum.py @@ -73,12 +73,6 @@ class EnumMemberString(str): it. """ - def __eq__(self, other: object) -> bool: - return str(self) == other - - def __hash__(self) -> int: - return super().__hash__() - class StrEnum(str, Enum, metaclass=ConstantEnumMeta): """A string constant / enumeration. For creating reusable, typed constants. From 9333186842910af21e1b76482cd50f6143d509c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niko=20F=C3=B6hr?= Date: Sun, 17 Mar 2024 17:43:51 +0200 Subject: [PATCH 13/74] add return type to _create_class in testmethods.py --- tests/unit/test_core/testmethods.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/test_core/testmethods.py b/tests/unit/test_core/testmethods.py index 1037d636..155811db 100644 --- a/tests/unit/test_core/testmethods.py +++ b/tests/unit/test_core/testmethods.py @@ -78,7 +78,7 @@ def m(self): return m - def _create_class(): + def _create_class() -> Type[Method]: clsname = get_new_classname() clskwargs = { "supported_platforms": supported_platforms, From 6cbe3544c3fbe78367d7a86f32adc8185ead8269 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niko=20F=C3=B6hr?= Date: Sun, 17 Mar 2024 18:14:03 +0200 Subject: [PATCH 14/74] ignore the DbusService and start_dbus_service types in conftest.py in other platforms than linux --- tests/integration/conftest.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 175da63a..dc2b38b5 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -13,9 +13,9 @@ from tests.integration.dbus_service import DBusService, start_dbus_service else: - class DBusService: ... + class DBusService: ... # type: ignore - def start_dbus_service(): ... + def start_dbus_service(): ... # type: ignore from wakepy.core import DBusAddress, DBusMethod From a65a799992d7fd401020de80c514afb6e808cf39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niko=20F=C3=B6hr?= Date: Sun, 17 Mar 2024 18:15:05 +0200 Subject: [PATCH 15/74] DBusMethod.of(): add return type --- wakepy/core/dbus.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wakepy/core/dbus.py b/wakepy/core/dbus.py index b71e04c5..cf9b30c4 100644 --- a/wakepy/core/dbus.py +++ b/wakepy/core/dbus.py @@ -148,7 +148,7 @@ class DBusMethod(NamedTuple): def of( self, addr: DBusAddress, - ): + ) -> DBusMethod: """Ties a DBusAddress to a DBusMethod, forming a completely defined DBusMethod. Returns a new DBusMethod object. """ From e7f820c3a30ad0bc9fc3b434b0d6b285ac41beda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niko=20F=C3=B6hr?= Date: Sun, 17 Mar 2024 18:28:40 +0200 Subject: [PATCH 16/74] define unnamed Methods with special constant instead of using None --- wakepy/core/activation.py | 2 +- wakepy/core/method.py | 14 +++++++++++--- wakepy/core/registry.py | 2 +- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/wakepy/core/activation.py b/wakepy/core/activation.py index 191a2787..e5e69ef6 100644 --- a/wakepy/core/activation.py +++ b/wakepy/core/activation.py @@ -555,7 +555,7 @@ def activate_method(method: Method) -> Tuple[MethodActivationResult, Heartbeat | If the `method` has method.heartbeat() implemented, and activation succeeds, this is a Heartbeat object. Otherwise, this is None. """ - if method.name is None: + if method._is_unnamed(): raise ValueError("Methods without a name may not be used to activate modes!") result = MethodActivationResult(success=False, method_name=method.name) diff --git a/wakepy/core/method.py b/wakepy/core/method.py index 78476f0f..adad0ed4 100644 --- a/wakepy/core/method.py +++ b/wakepy/core/method.py @@ -65,6 +65,10 @@ class MethodOutcome(StrEnum): FAILURE = auto() +unnamed = "__unnamed__" +"""Constant for defining unnamed Method(s)""" + + class Method(ABC, metaclass=MethodMeta): """Methods are objects that are used to switch modes. The phases for changing and being in a Mode is: @@ -95,10 +99,10 @@ class Method(ABC, metaclass=MethodMeta): create documentation. """ - name: str | None = None + name: str = unnamed """Human-readable name for the method. Used by end-users to define - the Methods used for entering a Mode, for example. If not None, must be - unique across all Methods available in the python process. Set to None if + the Methods used for entering a Mode, for example. If given, must be + unique across all Methods available in the python process. Leave unset if the Method should not be listed anywhere (e.g. when Method is meant to be subclassed).""" @@ -263,6 +267,10 @@ def __str__(self): def __repr__(self): return f"" + @classmethod + def _is_unnamed(cls): + return cls.name == unnamed + def select_methods( methods: MethodClsCollection, diff --git a/wakepy/core/registry.py b/wakepy/core/registry.py index df8b2302..06134c8e 100644 --- a/wakepy/core/registry.py +++ b/wakepy/core/registry.py @@ -45,7 +45,7 @@ class MethodRegistryError(RuntimeError): def register_method(method_class: Type[Method]): """Registers a subclass of Method to the method registry""" - if method_class.name is None: + if method_class._is_unnamed(): # Methods without a name will not be registered return From 18cf8cddc41450d930cb5e512b8f642ff9fcb352 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niko=20F=C3=B6hr?= Date: Sun, 17 Mar 2024 18:41:51 +0200 Subject: [PATCH 17/74] drop support for get_methods(None) there is probably no real use case for that and not supporting None makes the type annotations of the function easier --- tests/unit/test_core/test_registry.py | 6 +++--- wakepy/core/registry.py | 7 ++----- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/tests/unit/test_core/test_registry.py b/tests/unit/test_core/test_registry.py index 1181c9d9..7f4de034 100644 --- a/tests/unit/test_core/test_registry.py +++ b/tests/unit/test_core/test_registry.py @@ -64,9 +64,6 @@ class C(TestMethod): assert get_methods({"A", "B"}) == {A, B} assert get_methods({"C", "B"}) == {C, B} - # Asking None, getting empty list - assert get_methods(None) == [] - # Asking something that does not exists will raise KeyError with pytest.raises(ValueError, match=re.escape('No Method with name "foo" found!')): get_methods(["A", "foo"]) @@ -75,6 +72,9 @@ class C(TestMethod): with pytest.raises(TypeError): get_methods(4123) + with pytest.raises(TypeError): + get_methods(None) + @pytest.mark.usefixtures("provide_methods_a_f") def test_get_methods_for_mode(): diff --git a/wakepy/core/registry.py b/wakepy/core/registry.py index 06134c8e..36bde454 100644 --- a/wakepy/core/registry.py +++ b/wakepy/core/registry.py @@ -126,7 +126,7 @@ def get_method(method_name: str, mode: Optional[ModeName] = None) -> MethodCls: def get_methods( - names: Collection[str] | None = None, mode: Optional[ModeName] = None + names: Collection[str], mode: Optional[ModeName] = None ) -> Collection[MethodCls]: """Get a collection (list, tuple or set) of Method classes based on their names, and optionally the mode name. @@ -145,14 +145,11 @@ def get_methods( Raises ------ ValueError - Raised if any of the methods does not exist, or if any of the existing + Raised if any of the methods does not exist, or if any of the existing methods exists in multiple modes and the mode name (str) was not provided as argument to make the selection unambiguous. """ - if names is None: - return [] - if isinstance(names, list): return [get_method(name, mode) for name in names] elif isinstance(names, tuple): From 1902ad82f115048a0002060ca2faf37940662f80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niko=20F=C3=B6hr?= Date: Sun, 17 Mar 2024 18:52:08 +0200 Subject: [PATCH 18/74] fix 'unreachable code' warning in get_methods --- wakepy/core/registry.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/wakepy/core/registry.py b/wakepy/core/registry.py index 36bde454..27486464 100644 --- a/wakepy/core/registry.py +++ b/wakepy/core/registry.py @@ -156,8 +156,8 @@ def get_methods( return tuple(get_method(name, mode) for name in names) elif isinstance(names, set): return set(get_method(name, mode) for name in names) - - raise TypeError("`names` must be a list, tuple or set") + else: + raise TypeError("`names` must be a list, tuple or set") def get_methods_for_mode( From 949f8a9c711bd2de770e00d6743870037a652e96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niko=20F=C3=B6hr?= Date: Sun, 17 Mar 2024 19:20:14 +0200 Subject: [PATCH 19/74] improve type formatting in registry.py --- wakepy/core/registry.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/wakepy/core/registry.py b/wakepy/core/registry.py index 27486464..0c8f17ef 100644 --- a/wakepy/core/registry.py +++ b/wakepy/core/registry.py @@ -17,18 +17,17 @@ from __future__ import annotations import typing -from typing import List, Set, Tuple, Type, TypeVar, Union from .constants import ModeName if typing.TYPE_CHECKING: - from typing import Optional + from typing import List, Optional, Set, Tuple, Type, TypeAlias, TypeVar, Union from wakepy.core.method import Method, MethodCls T = TypeVar("T") - Collection = Union[List[T], Tuple[T, ...], Set[T]] + Collection: TypeAlias = Union[List[T], Tuple[T, ...], Set[T]] MethodDict = dict[str, MethodCls] MethodRegistry = dict[str, MethodDict] From e1b54c4ab7aec72a3aed3cb32e659068e1b6a328 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niko=20F=C3=B6hr?= Date: Sun, 17 Mar 2024 20:17:01 +0200 Subject: [PATCH 20/74] add overloads to get_methods --- wakepy/core/registry.py | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/wakepy/core/registry.py b/wakepy/core/registry.py index 0c8f17ef..ac5352c0 100644 --- a/wakepy/core/registry.py +++ b/wakepy/core/registry.py @@ -21,7 +21,17 @@ from .constants import ModeName if typing.TYPE_CHECKING: - from typing import List, Optional, Set, Tuple, Type, TypeAlias, TypeVar, Union + from typing import ( + List, + Optional, + Set, + Tuple, + Type, + TypeAlias, + TypeVar, + Union, + overload, + ) from wakepy.core.method import Method, MethodCls @@ -31,6 +41,7 @@ MethodDict = dict[str, MethodCls] MethodRegistry = dict[str, MethodDict] + _method_registry: MethodRegistry = dict() """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. @@ -124,6 +135,18 @@ def get_method(method_name: str, mode: Optional[ModeName] = None) -> MethodCls: return methods_from_all_modes[0] +@overload +def get_methods( + names: List[str], mode: Optional[ModeName] = None +) -> List[MethodCls]: ... +@overload +def get_methods( + names: Tuple[str, ...], mode: Optional[ModeName] = None +) -> Tuple[MethodCls, ...]: ... +@overload +def get_methods(names: Set[str], mode: Optional[ModeName] = None) -> Set[MethodCls]: ... + + def get_methods( names: Collection[str], mode: Optional[ModeName] = None ) -> Collection[MethodCls]: From c22b3027a855318cb2bc326098925d1d490cddf9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niko=20F=C3=B6hr?= Date: Sun, 17 Mar 2024 20:24:29 +0200 Subject: [PATCH 21/74] ignore purposeful wrong types in tests --- tests/unit/test_core/test_registry.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unit/test_core/test_registry.py b/tests/unit/test_core/test_registry.py index 7f4de034..6f378f2c 100644 --- a/tests/unit/test_core/test_registry.py +++ b/tests/unit/test_core/test_registry.py @@ -70,10 +70,10 @@ class C(TestMethod): # Using unsupported type raises TypeError with pytest.raises(TypeError): - get_methods(4123) + get_methods(4123) # type: ignore with pytest.raises(TypeError): - get_methods(None) + get_methods(None) # type: ignore @pytest.mark.usefixtures("provide_methods_a_f") From 67ea4c8435a0454d133e2a45230fbb111e3bcc5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niko=20F=C3=B6hr?= Date: Sun, 17 Mar 2024 20:31:10 +0200 Subject: [PATCH 22/74] import overload to fix errors in tests --- wakepy/core/registry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wakepy/core/registry.py b/wakepy/core/registry.py index ac5352c0..fac59741 100644 --- a/wakepy/core/registry.py +++ b/wakepy/core/registry.py @@ -17,7 +17,7 @@ from __future__ import annotations import typing - +from typing import overload from .constants import ModeName if typing.TYPE_CHECKING: From c5ac4e8fdd3f2d0bc059e68b158fc27c96b807b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niko=20F=C3=B6hr?= Date: Sun, 17 Mar 2024 20:32:11 +0200 Subject: [PATCH 23/74] ignore @overload in coverage --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 1140c06e..3bc7f9e1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -120,6 +120,7 @@ exclude_lines = [ "pragma: no cover", "if typing.TYPE_CHECKING:", "@(abc\\.)?abstractmethod", + "@(typing\\.)?overload", ] [tool.coverage.coverage_conditional_plugin.omit] From acd7190e60856680c8a17cbe551a358262fc667e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niko=20F=C3=B6hr?= Date: Mon, 18 Mar 2024 18:25:11 +0200 Subject: [PATCH 24/74] simplify the StrEnum --- wakepy/core/strenum.py | 34 ++++++++-------------------------- 1 file changed, 8 insertions(+), 26 deletions(-) diff --git a/wakepy/core/strenum.py b/wakepy/core/strenum.py index 436c4897..efff5b4b 100644 --- a/wakepy/core/strenum.py +++ b/wakepy/core/strenum.py @@ -7,7 +7,6 @@ from __future__ import annotations from enum import Enum, EnumMeta, auto -from types import MappingProxyType from typing import Any @@ -34,12 +33,6 @@ def __init__(cls, *_, unique=False): if unique: cls._check_uniqueness() - cls.__members__: MappingProxyType[str, Any] - for member in cls.__members__.values(): - # The .name is needed for the @unique decorator compatibility - # It is also part of enum member protocol, so let it be. - member.name = member._name_ - def _check_uniqueness(cls): vals = cls.__members__.values() if len(vals) > len(set(vals)): @@ -68,12 +61,6 @@ def values(self): return self.__members__.values -class EnumMemberString(str): - """dummy string subclass to make it possible to add custom attributes to - it. - """ - - class StrEnum(str, Enum, metaclass=ConstantEnumMeta): """A string constant / enumeration. For creating reusable, typed constants. @@ -122,20 +109,11 @@ def _generate_next_value_(name: str, *_) -> str: """ return name - def __new__(cls, val=None, *_): - """This is used to get rid of need for ".value" access: - - >>> StrEnum.FOO.value - 'foo' + def __str__(self) -> str: + return str.__str__(self) - It is possible to use - - >>> StrEnum.FOO - 'foo' - - instead - """ - return EnumMemberString(val) + def __hash__(self) -> int: + return super().__hash__() def __eq__(self, other: object) -> bool: # This was added just to make mypy happy. Without this mypy will @@ -143,6 +121,10 @@ def __eq__(self, other: object) -> bool: # In reality, the EnumMemberString.__eq__ is called in this case. return str(self) == other # pragma: no cover + @property + def name(self) -> str: + return self._name_ + __all__ = [ "StrEnum", From ab40f0cc90c0fe147df43cfe24c94ca6486129a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niko=20F=C3=B6hr?= Date: Mon, 18 Mar 2024 18:44:24 +0200 Subject: [PATCH 25/74] rename self to cls in ConstantEnumMeta method The 'self' refers to a class so perhaps clearer the the variable name is 'cls' --- wakepy/core/strenum.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/wakepy/core/strenum.py b/wakepy/core/strenum.py index efff5b4b..315fc2d2 100644 --- a/wakepy/core/strenum.py +++ b/wakepy/core/strenum.py @@ -38,27 +38,27 @@ def _check_uniqueness(cls): if len(vals) > len(set(vals)): raise ValueError("The values must be unique!") - def __contains__(self, value: Any) -> bool: + def __contains__(cls, value: Any) -> bool: """Provides the `val in SomeConstClass` containment check Parameters ---------- - self: + cls: This will be the (subclass) of the class using ConstantEnumMeta. If you use class Const(metaclass=ConstantEnumMeta): ... and - SomeConst(Const), self will be SomeConst; a class. + SomeConst(Const), cls will be SomeConst; a class. value: The `val` in the example """ - return value in self.values() + return value in cls.values() @property - def keys(self): - return self.__members__.keys + def keys(cls): + return cls.__members__.keys @property - def values(self): - return self.__members__.values + def values(cls): + return cls.__members__.values class StrEnum(str, Enum, metaclass=ConstantEnumMeta): From 96da81e562a9f151df43e29bad750989fd6e7043 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niko=20F=C3=B6hr?= Date: Mon, 18 Mar 2024 18:45:48 +0200 Subject: [PATCH 26/74] rename: StrEnumMeta was: ConstantEnumMeta (previously, StrEnum was called Constant) --- wakepy/core/strenum.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/wakepy/core/strenum.py b/wakepy/core/strenum.py index 315fc2d2..891f218d 100644 --- a/wakepy/core/strenum.py +++ b/wakepy/core/strenum.py @@ -10,7 +10,7 @@ from typing import Any -class ConstantEnumMeta(EnumMeta): +class StrEnumMeta(EnumMeta): """This metaclass is an extension of the basic Enum metaclass, and provides the following @@ -44,8 +44,8 @@ def __contains__(cls, value: Any) -> bool: Parameters ---------- cls: - This will be the (subclass) of the class using ConstantEnumMeta. - If you use class Const(metaclass=ConstantEnumMeta): ... and + This will be the (subclass) of the class using StrEnumMeta. + If you use class Const(metaclass=StrEnumMeta): ... and SomeConst(Const), cls will be SomeConst; a class. value: The `val` in the example @@ -61,7 +61,7 @@ def values(cls): return cls.__members__.values -class StrEnum(str, Enum, metaclass=ConstantEnumMeta): +class StrEnum(str, Enum, metaclass=StrEnumMeta): """A string constant / enumeration. For creating reusable, typed constants. Properties From 9677bb0b5d6ecd5f27b4a4257edb5c69e91f0bbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niko=20F=C3=B6hr?= Date: Mon, 18 Mar 2024 18:46:39 +0200 Subject: [PATCH 27/74] update StrEnumMeta docstring There was old information. --- wakepy/core/strenum.py | 1 - 1 file changed, 1 deletion(-) diff --git a/wakepy/core/strenum.py b/wakepy/core/strenum.py index 891f218d..8c80ed4d 100644 --- a/wakepy/core/strenum.py +++ b/wakepy/core/strenum.py @@ -16,7 +16,6 @@ class StrEnumMeta(EnumMeta): 1) Containment check for enumeration member values; `val in SomeClass` 2) `unique` parameter when creating constants. - 3) Support for (custom) string-type members in Enums """ @classmethod From c5d686da603e45f2673b5be78bdca4f02848d1bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niko=20F=C3=B6hr?= Date: Mon, 18 Mar 2024 18:59:08 +0200 Subject: [PATCH 28/74] fix mypy errors related to StageName --- tests/unit/test_core/test_activation/test_activation.py | 5 +++++ wakepy/core/activation.py | 7 ++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/tests/unit/test_core/test_activation/test_activation.py b/tests/unit/test_core/test_activation/test_activation.py index eebc6c6e..a8653824 100644 --- a/tests/unit/test_core/test_activation/test_activation.py +++ b/tests/unit/test_core/test_activation/test_activation.py @@ -6,6 +6,7 @@ import datetime as dt import os import re +import typing from contextlib import contextmanager from unittest.mock import Mock @@ -29,6 +30,7 @@ ) from wakepy.core.activation import ( StageName, + StageNameValue, WakepyFakeSuccess, activate_method, activate_mode, @@ -602,6 +604,9 @@ def test_stagename(): assert StageName.PLATFORM_SUPPORT == "PLATFORM_SUPPORT" assert StageName.ACTIVATION == "ACTIVATION" assert StageName.REQUIREMENTS == "REQUIREMENTS" + assert set(typing.get_args(StageNameValue)) == { + member.value for member in StageName + } # These are the only "falsy" values for WAKEPY_FAKE_SUCCESS diff --git a/wakepy/core/activation.py b/wakepy/core/activation.py index e5e69ef6..d74bcc7f 100644 --- a/wakepy/core/activation.py +++ b/wakepy/core/activation.py @@ -54,6 +54,11 @@ class StageName(StrEnum): ACTIVATION = auto() +StageNameValue = typing.Literal[ + "NONE", "PLATFORM_SUPPORT", "REQUIREMENTS", "ACTIVATION" +] + + class ActivationResult: """The ActivationResult is responsible of keeping track on the possibly successful (max 1), failed and unused methods and providing different views @@ -184,7 +189,7 @@ def list_methods( def query( self, success: Sequence[bool | None] = (True, False, None), - fail_stages: Sequence[StageName] = ( + fail_stages: Sequence[StageName | StageNameValue] = ( StageName.PLATFORM_SUPPORT, StageName.REQUIREMENTS, StageName.ACTIVATION, From a853e0df367ab1a20296f813276d01629230a231 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niko=20F=C3=B6hr?= Date: Mon, 18 Mar 2024 19:10:37 +0200 Subject: [PATCH 29/74] format --- wakepy/core/registry.py | 1 + 1 file changed, 1 insertion(+) diff --git a/wakepy/core/registry.py b/wakepy/core/registry.py index fac59741..1db8e174 100644 --- a/wakepy/core/registry.py +++ b/wakepy/core/registry.py @@ -18,6 +18,7 @@ import typing from typing import overload + from .constants import ModeName if typing.TYPE_CHECKING: From 8f1921a8b60a3c2eda3ec26a5f41d7e10518f5ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niko=20F=C3=B6hr?= Date: Mon, 18 Mar 2024 19:16:49 +0200 Subject: [PATCH 30/74] fix mypy: StrEnumMeta._check_uniqueness() --- wakepy/core/strenum.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/wakepy/core/strenum.py b/wakepy/core/strenum.py index 8c80ed4d..68a9106c 100644 --- a/wakepy/core/strenum.py +++ b/wakepy/core/strenum.py @@ -7,7 +7,7 @@ from __future__ import annotations from enum import Enum, EnumMeta, auto -from typing import Any +from typing import Any, ValuesView class StrEnumMeta(EnumMeta): @@ -33,7 +33,7 @@ def __init__(cls, *_, unique=False): cls._check_uniqueness() def _check_uniqueness(cls): - vals = cls.__members__.values() + vals: ValuesView[Enum] = cls.__members__.values() if len(vals) > len(set(vals)): raise ValueError("The values must be unique!") From 793e9d54d0b996d02bcdee3ae0ef1914fb0117d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niko=20F=C3=B6hr?= Date: Mon, 18 Mar 2024 19:20:31 +0200 Subject: [PATCH 31/74] ignore mypy error in purposeful ValueError --- tests/unit/test_core/test_activation/test_prioritization.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/unit/test_core/test_activation/test_prioritization.py b/tests/unit/test_core/test_activation/test_prioritization.py index e44d41a6..2737c936 100644 --- a/tests/unit/test_core/test_activation/test_prioritization.py +++ b/tests/unit/test_core/test_activation/test_prioritization.py @@ -70,7 +70,10 @@ def test_check_methods_priority(): TypeError, match=re.escape("methods_priority must be a list[str | set[str]]!"), ): - check_methods_priority(methods_priority=[MethodA], methods=methods) + check_methods_priority( + methods_priority=[MethodA], # type: ignore + methods=methods, + ) @pytest.mark.usefixtures("provide_methods_a_f") From 08ac3a672d64b5854c972de2f7c065ae9ed41af6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niko=20F=C3=B6hr?= Date: Mon, 18 Mar 2024 20:37:15 +0200 Subject: [PATCH 32/74] MethodsPriorityOrder: List -> Sequence Change, since used as input parameter and Sequence is covariant whereas List is invariant. --- wakepy/core/activation.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/wakepy/core/activation.py b/wakepy/core/activation.py index d74bcc7f..962340bd 100644 --- a/wakepy/core/activation.py +++ b/wakepy/core/activation.py @@ -23,7 +23,7 @@ import datetime as dt import typing from dataclasses import dataclass -from typing import List, Set, Union +from typing import List, Sequence, Set, Union from .constants import PlatformName from .dbus import DBusAdapter @@ -33,13 +33,13 @@ from .strenum import StrEnum, auto if typing.TYPE_CHECKING: - from typing import Optional, Sequence, Tuple, Type + from typing import Optional, Tuple, Type from .method import MethodCls """The strings in MethodsPriorityOrder are names of wakepy.Methods or the asterisk ('*').""" -MethodsPriorityOrder = List[Union[str, Set[str]]] +MethodsPriorityOrder = Sequence[Union[str, Set[str]]] class StageName(StrEnum): From 873454376c3e33a6e7d221db7838b99983f99af0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niko=20F=C3=B6hr?= Date: Mon, 18 Mar 2024 20:39:25 +0200 Subject: [PATCH 33/74] mypy fix: importing PlatformName --- tests/unit/test_core/conftest.py | 2 +- tests/unit/test_core/test_activation/test_prioritization.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unit/test_core/conftest.py b/tests/unit/test_core/conftest.py index 94c9ebca..5d8cd1db 100644 --- a/tests/unit/test_core/conftest.py +++ b/tests/unit/test_core/conftest.py @@ -1,6 +1,6 @@ import pytest -from wakepy.core.method import Method, PlatformName +from wakepy.core import Method, PlatformName # B, D, E FIRST_MODE = "first_mode" diff --git a/tests/unit/test_core/test_activation/test_prioritization.py b/tests/unit/test_core/test_activation/test_prioritization.py index 2737c936..1966d5d7 100644 --- a/tests/unit/test_core/test_activation/test_prioritization.py +++ b/tests/unit/test_core/test_activation/test_prioritization.py @@ -2,13 +2,13 @@ import pytest +from wakepy.core import PlatformName from wakepy.core.activation import ( check_methods_priority, get_prioritized_methods, get_prioritized_methods_groups, sort_methods_by_priority, ) -from wakepy.core.method import PlatformName from wakepy.core.registry import get_methods From 323ec8bb88a27cc8a435d24063059bc89f6825f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niko=20F=C3=B6hr?= Date: Mon, 18 Mar 2024 20:41:23 +0200 Subject: [PATCH 34/74] fix imports for mypy --- tests/unit/test_core/test_mode.py | 9 ++------- tests/unit/test_core/test_platform.py | 3 ++- wakepy/core/dbus.py | 2 +- 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/tests/unit/test_core/test_mode.py b/tests/unit/test_core/test_mode.py index ee6edcd2..979f9214 100644 --- a/tests/unit/test_core/test_mode.py +++ b/tests/unit/test_core/test_mode.py @@ -3,15 +3,10 @@ import pytest from tests.unit.test_core.testmethods import get_test_method_class +from wakepy.core import ActivationResult from wakepy.core.dbus import DBusAdapter from wakepy.core.heartbeat import Heartbeat -from wakepy.core.mode import ( - ActivationResult, - Mode, - ModeController, - ModeExit, - handle_activation_fail, -) +from wakepy.core.mode import Mode, ModeController, ModeExit, handle_activation_fail def mocks_for_test_mode(): diff --git a/tests/unit/test_core/test_platform.py b/tests/unit/test_core/test_platform.py index 5eb6b29f..4dd9251e 100644 --- a/tests/unit/test_core/test_platform.py +++ b/tests/unit/test_core/test_platform.py @@ -2,7 +2,8 @@ import pytest -from wakepy.core.platform import PlatformName, get_current_platform +from wakepy.core import PlatformName +from wakepy.core.platform import get_current_platform class TestGetCurrentPlatform: diff --git a/wakepy/core/dbus.py b/wakepy/core/dbus.py index cf9b30c4..edc219c5 100644 --- a/wakepy/core/dbus.py +++ b/wakepy/core/dbus.py @@ -18,7 +18,7 @@ import typing from typing import Any, Dict, List, NamedTuple, Optional, Tuple, Type, Union -from .constants import BusType +from .constants import BusType as BusType CallArguments = Optional[Union[Dict[str, Any], Tuple[Any, ...], List[Any]]] From b402eff19635073ec38689c1f9ff229e76badf94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niko=20F=C3=B6hr?= Date: Mon, 18 Mar 2024 20:53:21 +0200 Subject: [PATCH 35/74] refactor: dbus_method fixture to conftest Make this available to other tests as well --- tests/unit/test_core/conftest.py | 14 ++++++- tests/unit/test_core/test_calls.py | 64 ++++++++++++------------------ 2 files changed, 39 insertions(+), 39 deletions(-) diff --git a/tests/unit/test_core/conftest.py b/tests/unit/test_core/conftest.py index 5d8cd1db..37855cd2 100644 --- a/tests/unit/test_core/conftest.py +++ b/tests/unit/test_core/conftest.py @@ -1,6 +1,6 @@ import pytest -from wakepy.core import Method, PlatformName +from wakepy.core import DBusAddress, DBusMethod, Method, PlatformName # B, D, E FIRST_MODE = "first_mode" @@ -76,3 +76,15 @@ class MethodE(TestMethod): class MethodF(TestMethod): name = "F" mode = SECOND_MODE + + +@pytest.fixture +def service(): + return DBusAddress(path="/foo", service="wakepy.foo", interface="/foo") + + +@pytest.fixture +def dbus_method(service: DBusAddress): + return DBusMethod( + name="test-method", signature="isi", params=("first", "second", "third") + ).of(service) diff --git a/tests/unit/test_core/test_calls.py b/tests/unit/test_core/test_calls.py index 52896eb7..04b56793 100644 --- a/tests/unit/test_core/test_calls.py +++ b/tests/unit/test_core/test_calls.py @@ -5,18 +5,6 @@ from wakepy.core import DBusAddress, DBusMethod, DBusMethodCall -@pytest.fixture -def service(): - return DBusAddress(path="/foo", service="wakepy.foo", interface="/foo") - - -@pytest.fixture -def method(service): - return DBusMethod( - name="test-method", signature="isi", params=("first", "second", "third") - ).of(service) - - @pytest.fixture def method_without_params(service): service = DBusAddress(path="/foo", service="wakepy.foo", interface="/foo") @@ -26,43 +14,43 @@ def method_without_params(service): ).of(service) -def test_dbusmethod_args_none(method: DBusMethod): - call = DBusMethodCall(method, args=None) +def test_dbusmethod_args_none(dbus_method: DBusMethod): + call = DBusMethodCall(dbus_method, args=None) assert call.args == tuple() -def test_dbusmethod_args_missing(method: DBusMethod): - call = DBusMethodCall(method) +def test_dbusmethod_args_missing(dbus_method: DBusMethod): + call = DBusMethodCall(dbus_method) assert call.args == tuple() -def test_dbusmethod_args_tuple(method: DBusMethod): +def test_dbusmethod_args_tuple(dbus_method: DBusMethod): args = (1, "2", 3) - call = DBusMethodCall(method, args=args) + call = DBusMethodCall(dbus_method, args=args) assert call.args == args -def test_dbusmethod_args_tuple_too_long(method: DBusMethod): +def test_dbusmethod_args_tuple_too_long(dbus_method: DBusMethod): args = (1, "2", 3, 4) with pytest.raises( ValueError, match=re.escape("Expected args to have 3 items! (has: 4)"), ): - DBusMethodCall(method, args=args) + DBusMethodCall(dbus_method, args=args) -def test_dbusmethod_args_tuple_too_short(method: DBusMethod): +def test_dbusmethod_args_tuple_too_short(dbus_method: DBusMethod): args = (1, "2") with pytest.raises( ValueError, match=re.escape("Expected args to have 3 items! (has: 2)"), ): - DBusMethodCall(method, args=args) + DBusMethodCall(dbus_method, args=args) -def test_dbusmethod_args_list(method: DBusMethod): +def test_dbusmethod_args_list(dbus_method: DBusMethod): args = [1, "2", 3] - call = DBusMethodCall(method, args=args) + call = DBusMethodCall(dbus_method, args=args) assert call.args == (1, "2", 3) @@ -81,27 +69,27 @@ def test_dbusmethod_args_dict_method_without_params( DBusMethodCall(method_without_params, args=args) -def test_dbusmethod_args_dict_too_many_keys(method: DBusMethod): +def test_dbusmethod_args_dict_too_many_keys(dbus_method: DBusMethod): args = dict(first=1, second="2", third=3, fourth=4) with pytest.raises( ValueError, match=re.escape("Expected args to have 3 items! (has: 4)"), ): - DBusMethodCall(method, args=args) + DBusMethodCall(dbus_method, args=args) -def test_dbusmethod_args_dict_too_few_keys(method: DBusMethod): +def test_dbusmethod_args_dict_too_few_keys(dbus_method: DBusMethod): args = dict(first=1, second="2") with pytest.raises( ValueError, match=re.escape("Expected args to have 3 items! (has: 2)"), ): - DBusMethodCall(method, args=args) + DBusMethodCall(dbus_method, args=args) -def test_dbusmethod_args_dict_wrong_keys(method: DBusMethod): +def test_dbusmethod_args_dict_wrong_keys(dbus_method: DBusMethod): args = dict(first=1, second="2", fifth="2") with pytest.raises( @@ -111,24 +99,24 @@ def test_dbusmethod_args_dict_wrong_keys(method: DBusMethod): "Expected: ('first', 'second', 'third'). Got: ('first', 'second', 'fifth')" ), ): - DBusMethodCall(method, args=args) + DBusMethodCall(dbus_method, args=args) -def test_dbusmethod_args_dict(method: DBusMethod): +def test_dbusmethod_args_dict(dbus_method: DBusMethod): args = dict(first=1, second="2", third=3) - call = DBusMethodCall(method, args=args) + call = DBusMethodCall(dbus_method, args=args) assert call.args == (1, "2", 3) -def test_dbusmethod_args_dict_different_order(method: DBusMethod): +def test_dbusmethod_args_dict_different_order(dbus_method: DBusMethod): args = dict(third=3, first=1, second="2") - call = DBusMethodCall(method, args=args) + call = DBusMethodCall(dbus_method, args=args) assert call.args == (1, "2", 3) -def test_dbusmethod_get_kwargs(method: DBusMethod): +def test_dbusmethod_get_kwargs(dbus_method: DBusMethod): args = (1, "2", 3) - call = DBusMethodCall(method, args=args) + call = DBusMethodCall(dbus_method, args=args) assert call.get_kwargs() == dict(first=1, second="2", third=3) @@ -139,7 +127,7 @@ def test_dbusmethod_get_kwargs_noparams(method_without_params: DBusMethod): assert call.get_kwargs() is None -def test_dbusmethod_string_representation(method: DBusMethod): +def test_dbusmethod_string_representation(dbus_method: DBusMethod): args = (1, "2", 3) - call = DBusMethodCall(method, args=args) + call = DBusMethodCall(dbus_method, args=args) assert call.__repr__() == "" From 600be89445b0aa6af9a69d0761e1c44d38fdf160 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niko=20F=C3=B6hr?= Date: Mon, 18 Mar 2024 20:53:45 +0200 Subject: [PATCH 36/74] fix a type annotation in test_process_dbus_call --- tests/unit/test_core/test_method.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/tests/unit/test_core/test_method.py b/tests/unit/test_core/test_method.py index 62d63c84..c3a453fe 100644 --- a/tests/unit/test_core/test_method.py +++ b/tests/unit/test_core/test_method.py @@ -1,10 +1,17 @@ +from __future__ import annotations + import re +import typing import pytest +from wakepy.core import DBusMethodCall from wakepy.core.method import Method, select_methods from wakepy.core.registry import MethodRegistryError, get_method, get_methods +if typing.TYPE_CHECKING: + from wakepy.core import DBusMethod + class TestMethod(Method): __test__ = False # for pytest @@ -171,7 +178,7 @@ def test_method_string_representations(): assert method.__repr__() == f"" -def test_process_dbus_call(): +def test_process_dbus_call(dbus_method: DBusMethod): method = Method() # when there is no dbus adapter.. assert method._dbus_adapter is None @@ -180,4 +187,4 @@ def test_process_dbus_call(): RuntimeError, match=".*cannot process dbus method call.*as it does not have a DBusAdapter", ): - assert method.process_dbus_call(None) + assert method.process_dbus_call(DBusMethodCall(dbus_method)) From 87fae12c2940acbee187177cee15b1cb978a4553 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niko=20F=C3=B6hr?= Date: Mon, 18 Mar 2024 20:58:53 +0200 Subject: [PATCH 37/74] ignore mypy type errors when testing that Method.has_enter, .has_exit cannot be assigned to any value --- tests/unit/test_core/test_method.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/unit/test_core/test_method.py b/tests/unit/test_core/test_method.py index c3a453fe..84204298 100644 --- a/tests/unit/test_core/test_method.py +++ b/tests/unit/test_core/test_method.py @@ -93,14 +93,14 @@ def enter_mode(self): # The .has_enter, .has_exit or .has_heartbeat should be strictly read-only with pytest.raises(AttributeError): - method.has_enter = False + method.has_enter = False # type: ignore with pytest.raises(AttributeError): - method.has_exit = True + method.has_exit = True # type: ignore # Same holds for classes with pytest.raises(AttributeError): - MethodWithEnter.has_enter = False + MethodWithEnter.has_enter = False # type: ignore @pytest.mark.usefixtures("empty_method_registry") From 0d24d1393697938b4faec1d753f77a765c20221e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niko=20F=C3=B6hr?= Date: Mon, 18 Mar 2024 20:59:14 +0200 Subject: [PATCH 38/74] add type annotations to Method.has_enter, .has_exit and .has_hearbeat --- wakepy/core/method.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/wakepy/core/method.py b/wakepy/core/method.py index adad0ed4..fd7ccc21 100644 --- a/wakepy/core/method.py +++ b/wakepy/core/method.py @@ -250,15 +250,15 @@ def process_dbus_call(self, call: DBusMethodCall) -> Any: return self._dbus_adapter.process(call) @property - def has_enter(self): + def has_enter(self) -> bool: return self._has_enter @property - def has_exit(self): + def has_exit(self) -> bool: return self._has_exit @property - def has_heartbeat(self): + def has_heartbeat(self) -> bool: return self._has_heartbeat def __str__(self): From d48c5107a3ab226f8469ad91db93968d811d3eba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niko=20F=C3=B6hr?= Date: Mon, 18 Mar 2024 21:08:21 +0200 Subject: [PATCH 39/74] mypy: ignore the purposeful re-defines of SomeMethod in test_method.py --- tests/unit/test_core/test_method.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unit/test_core/test_method.py b/tests/unit/test_core/test_method.py index 84204298..dc10d221 100644 --- a/tests/unit/test_core/test_method.py +++ b/tests/unit/test_core/test_method.py @@ -115,14 +115,14 @@ class SomeMethod(TestMethod): MethodRegistryError, match=re.escape('Duplicate Method name "Some name"') ): - class SomeMethod(TestMethod): # noqa:F811 + class SomeMethod(TestMethod): # type: ignore # noqa:F811 name = somename testutils.empty_method_registry(monkeypatch) # Now as the registry is empty it is possible to define method with # the same name again - class SomeMethod(TestMethod): # noqa:F811 + class SomeMethod(TestMethod): # type: ignore # noqa:F811 name = somename From 69b5a3514185184106b3674fb24119d028ae2c49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niko=20F=C3=B6hr?= Date: Mon, 18 Mar 2024 21:09:27 +0200 Subject: [PATCH 40/74] ignore the wrong value in on_fail in tests the wrong value is there on purpose --- tests/unit/test_core/test_mode.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/unit/test_core/test_mode.py b/tests/unit/test_core/test_mode.py index 979f9214..420ed797 100644 --- a/tests/unit/test_core/test_mode.py +++ b/tests/unit/test_core/test_mode.py @@ -202,7 +202,10 @@ def _assert_context_manager_used_correctly(mocks, mode): def test_handle_activation_fail_bad_on_fail_value(): with pytest.raises(ValueError, match="on_fail must be one of"): - handle_activation_fail(on_fail="foo", result=Mock(spec_set=ActivationResult)) + handle_activation_fail( + on_fail="foo", # type: ignore + result=Mock(spec_set=ActivationResult), + ) def test_modecontroller(monkeypatch): From 08b63bbd86441ce100c4f8c75b91f7403b963e42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niko=20F=C3=B6hr?= Date: Mon, 18 Mar 2024 21:10:18 +0200 Subject: [PATCH 41/74] ignore the redefs of MyConst in test_strenum.py --- tests/unit/test_core/test_strenum.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unit/test_core/test_strenum.py b/tests/unit/test_core/test_strenum.py index afa4af30..fe6d5784 100644 --- a/tests/unit/test_core/test_strenum.py +++ b/tests/unit/test_core/test_strenum.py @@ -57,13 +57,13 @@ class MyConst(StrEnum, unique=True): # uniqueness is asked with pytest.raises(ValueError): - class MyConst(StrEnum, unique=True): + class MyConst(StrEnum, unique=True): # type: ignore FOO = "fooval" BAR = "fooval" # It should be possible to define duplicate values if uniqueness is not # asked - class MyConst(StrEnum): + class MyConst(StrEnum): # type: ignore FOO = "fooval" ANOTHER_FOO = "fooval" From 5337c5957c7a2e99e02561ffd91dda306af9353a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niko=20F=C3=B6hr?= Date: Mon, 18 Mar 2024 21:11:40 +0200 Subject: [PATCH 42/74] require types-colorame --- pyproject.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 3bc7f9e1..6d3df086 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,7 +38,8 @@ dev = [ "IPython", "invoke==2.2.0", # Colorama is used with the tasks.py (invoke commands) - "colorama==0.4.6" + "colorama==0.4.6", + "types-colorama", # for mypy ] # For building documentation doc = [ From 90732881b5dd2055d5b40a19958d190e3342221b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niko=20F=C3=B6hr?= Date: Mon, 18 Mar 2024 21:12:58 +0200 Subject: [PATCH 43/74] fix mypy complaining about from invoke import task This is how the invoke docs explain task should be imported --- tasks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tasks.py b/tasks.py index e97f6c5a..acb30337 100644 --- a/tasks.py +++ b/tasks.py @@ -25,7 +25,7 @@ import typing from colorama import Fore -from invoke import task +from invoke import task # type: ignore if typing.TYPE_CHECKING: from invoke.runners import Result From c830ffb6cd532d8abd8f89ad61ea6dce430b23fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niko=20F=C3=B6hr?= Date: Mon, 18 Mar 2024 21:23:48 +0200 Subject: [PATCH 44/74] remove duplicated tests --- tests/unit/test_core/test_strenum.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/tests/unit/test_core/test_strenum.py b/tests/unit/test_core/test_strenum.py index fe6d5784..efc5d706 100644 --- a/tests/unit/test_core/test_strenum.py +++ b/tests/unit/test_core/test_strenum.py @@ -45,14 +45,6 @@ class MyConst(StrEnum, unique=True): BAR = "barval" BAZ = auto() - # Any string valued constant is - # added as a string - assert MyConst.FOO == "fooval" - assert MyConst.BAR == "barval" - assert MyConst.BAZ == "BAZ" - for obj in (MyConst.FOO, MyConst.BAR, MyConst.BAZ): - assert isinstance(obj, str) - # This should raise exception as the 'fooval' value is used twice and # uniqueness is asked with pytest.raises(ValueError): From bb13e0fed6cbf64c736e65dff2f86f7ec57c55d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niko=20F=C3=B6hr?= Date: Mon, 18 Mar 2024 21:25:21 +0200 Subject: [PATCH 45/74] split a test in three the uniqueneess of StrEnum --- tests/unit/test_core/test_strenum.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/unit/test_core/test_strenum.py b/tests/unit/test_core/test_strenum.py index efc5d706..1ca06b6f 100644 --- a/tests/unit/test_core/test_strenum.py +++ b/tests/unit/test_core/test_strenum.py @@ -35,16 +35,14 @@ class MyConst(StrEnum): assert isinstance(MyConst.BAR, str) -def test_constant_uniqueness(): - # It is possible to use the @unique decorator - # from the enum package - +def test_constant_uniqueness_with_unique_values(): # This should cause no problems class MyConst(StrEnum, unique=True): FOO = "fooval" BAR = "barval" - BAZ = auto() + +def test_constant_uniqueness_with_non_unique_values(): # This should raise exception as the 'fooval' value is used twice and # uniqueness is asked with pytest.raises(ValueError): @@ -53,6 +51,8 @@ class MyConst(StrEnum, unique=True): # type: ignore FOO = "fooval" BAR = "fooval" + +def test_constant_duplicates_non_unique_constraint(): # It should be possible to define duplicate values if uniqueness is not # asked class MyConst(StrEnum): # type: ignore From a41ce659c4ba3bf388b22f5588dc9ce69c5847f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niko=20F=C3=B6hr?= Date: Mon, 18 Mar 2024 21:26:04 +0200 Subject: [PATCH 46/74] rename: test_strenum_* old: test_constant the class was previously Constant. Now it's StrEnum --- tests/unit/test_core/test_strenum.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/unit/test_core/test_strenum.py b/tests/unit/test_core/test_strenum.py index 1ca06b6f..c2026ced 100644 --- a/tests/unit/test_core/test_strenum.py +++ b/tests/unit/test_core/test_strenum.py @@ -5,7 +5,7 @@ from wakepy.core.strenum import StrEnum -def test_constant_basic_functionality(): +def test_strenum_basic_functionality(): class MyConst(StrEnum): FOO = "fooval" @@ -23,7 +23,7 @@ class MyConst(StrEnum): assert "FOO" in MyConst.__members__.keys() -def test_constant_auto(): +def test_strenum_auto(): class MyConst(StrEnum): BAR = auto() @@ -35,14 +35,14 @@ class MyConst(StrEnum): assert isinstance(MyConst.BAR, str) -def test_constant_uniqueness_with_unique_values(): +def test_strenum_uniqueness_with_unique_values(): # This should cause no problems class MyConst(StrEnum, unique=True): FOO = "fooval" BAR = "barval" -def test_constant_uniqueness_with_non_unique_values(): +def test_strenum_uniqueness_with_non_unique_values(): # This should raise exception as the 'fooval' value is used twice and # uniqueness is asked with pytest.raises(ValueError): @@ -52,7 +52,7 @@ class MyConst(StrEnum, unique=True): # type: ignore BAR = "fooval" -def test_constant_duplicates_non_unique_constraint(): +def test_strenum_duplicates_non_unique_constraint(): # It should be possible to define duplicate values if uniqueness is not # asked class MyConst(StrEnum): # type: ignore From b2ddfd1a4f0b86323c3c772151ff601b7babb114 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niko=20F=C3=B6hr?= Date: Mon, 18 Mar 2024 21:26:53 +0200 Subject: [PATCH 47/74] remove unnecessary type: ignores --- tests/unit/test_core/test_strenum.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unit/test_core/test_strenum.py b/tests/unit/test_core/test_strenum.py index c2026ced..171d4a70 100644 --- a/tests/unit/test_core/test_strenum.py +++ b/tests/unit/test_core/test_strenum.py @@ -47,7 +47,7 @@ def test_strenum_uniqueness_with_non_unique_values(): # uniqueness is asked with pytest.raises(ValueError): - class MyConst(StrEnum, unique=True): # type: ignore + class MyConst(StrEnum, unique=True): FOO = "fooval" BAR = "fooval" @@ -55,7 +55,7 @@ class MyConst(StrEnum, unique=True): # type: ignore def test_strenum_duplicates_non_unique_constraint(): # It should be possible to define duplicate values if uniqueness is not # asked - class MyConst(StrEnum): # type: ignore + class MyConst(StrEnum): FOO = "fooval" ANOTHER_FOO = "fooval" From bec4c4f0632800379f0964c788975b124c6317f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niko=20F=C3=B6hr?= Date: Mon, 18 Mar 2024 21:28:21 +0200 Subject: [PATCH 48/74] mypy: add missing self in methods in test_method.py --- tests/unit/test_core/test_method.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unit/test_core/test_method.py b/tests/unit/test_core/test_method.py index dc10d221..7405b9a8 100644 --- a/tests/unit/test_core/test_method.py +++ b/tests/unit/test_core/test_method.py @@ -64,7 +64,7 @@ def exit_mode(self): assert method3.has_heartbeat class SubWithEnterAndHeart(WithJustHeartBeat): - def enter_mode(): + def enter_mode(self): return method4 = SubWithEnterAndHeart() @@ -73,7 +73,7 @@ def enter_mode(): assert not method4.has_exit class SubWithEnterAndExit(WithEnterAndExit): - def enter_mode(): + def enter_mode(self): return 123 method5 = SubWithEnterAndExit() From 5d634b23ee5602e7484ea8cd41a9bad4045fc16b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niko=20F=C3=B6hr?= Date: Mon, 18 Mar 2024 22:18:06 +0200 Subject: [PATCH 49/74] Don't inheric Mode from ABC no need to. --- wakepy/core/mode.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/wakepy/core/mode.py b/wakepy/core/mode.py index d769f182..c32532cd 100644 --- a/wakepy/core/mode.py +++ b/wakepy/core/mode.py @@ -2,7 +2,6 @@ import typing import warnings -from abc import ABC from .activation import ActivationResult, activate_mode, deactivate_method from .dbus import get_dbus_adapter @@ -98,7 +97,7 @@ def deactivate(self) -> bool: return True -class Mode(ABC): +class Mode: """A mode is something that is activated (entered in) and deactivated (exited from). Each Mode instance is created with a set of Method classes, and each one of the Methods may be used to activate the Mode. There are From 9657b2adfc3266104d0b09a69f4c54f98088f87a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niko=20F=C3=B6hr?= Date: Mon, 18 Mar 2024 22:42:17 +0200 Subject: [PATCH 50/74] mypy ignore unreachable * these parts are unreachable, and that is known. Just testing that they're really are unreachable --- tests/unit/test_core/test_mode.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/unit/test_core/test_mode.py b/tests/unit/test_core/test_mode.py index 420ed797..a81a33fa 100644 --- a/tests/unit/test_core/test_mode.py +++ b/tests/unit/test_core/test_mode.py @@ -131,7 +131,7 @@ def test_mode_exits_with_modeexit(): ) as mode: testval = 2 raise ModeExit - testval = 0 # never hit + testval = 0 # type: ignore # (never hit) assert testval == 2 @@ -149,7 +149,7 @@ def test_mode_exits_with_modeexit_with_args(): ) as mode: testval = 3 raise ModeExit("FOOO") - testval = 0 # never hit + testval = 0 # type: ignore # (never hit) assert testval == 3 @@ -170,7 +170,7 @@ class MyException(Exception): ... ) as mode: testval = 4 raise MyException - testval = 0 + testval = 0 # type: ignore # (never hit) assert testval == 4 From ed86b3d9a0299a40994dcc676f69a0154d424f02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niko=20F=C3=B6hr?= Date: Mon, 18 Mar 2024 22:46:28 +0200 Subject: [PATCH 51/74] fix few mypy unreachable false positives --- tests/unit/test_core/test_mode.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/unit/test_core/test_mode.py b/tests/unit/test_core/test_mode.py index a81a33fa..b2ba90e4 100644 --- a/tests/unit/test_core/test_mode.py +++ b/tests/unit/test_core/test_mode.py @@ -1,3 +1,4 @@ +import typing from unittest.mock import Mock, call import pytest @@ -8,6 +9,9 @@ from wakepy.core.heartbeat import Heartbeat from wakepy.core.mode import Mode, ModeController, ModeExit, handle_activation_fail +if typing.TYPE_CHECKING: + from typing import Tuple, Type + def mocks_for_test_mode(): # Setup test @@ -30,7 +34,7 @@ def mocks_for_test_mode(): return mocks -def get_mocks_and_testmode(): +def get_mocks_and_testmode() -> Tuple[Mock, Type[Mode]]: # Setup mocks mocks = mocks_for_test_mode() From 90bd275323315542a1002f30ec57955a48c262a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niko=20F=C3=B6hr?= Date: Sat, 23 Mar 2024 11:07:10 +0200 Subject: [PATCH 52/74] add missing __future__.annotations import --- tests/unit/test_core/test_mode.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/unit/test_core/test_mode.py b/tests/unit/test_core/test_mode.py index b2ba90e4..40978a48 100644 --- a/tests/unit/test_core/test_mode.py +++ b/tests/unit/test_core/test_mode.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import typing from unittest.mock import Mock, call From 74bbcdbf1a88dd0807dada3d8ef7d9c71348773f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niko=20F=C3=B6hr?= Date: Sat, 23 Mar 2024 11:18:55 +0200 Subject: [PATCH 53/74] refactor get_new_classname Use module level Counter instead of function-level attribute dict --- tests/unit/test_core/testmethods.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/unit/test_core/testmethods.py b/tests/unit/test_core/testmethods.py index 155811db..54d05749 100644 --- a/tests/unit/test_core/testmethods.py +++ b/tests/unit/test_core/testmethods.py @@ -1,6 +1,7 @@ from __future__ import annotations import itertools +from collections import Counter from typing import Iterable, Type from wakepy.core import CURRENT_PLATFORM @@ -10,13 +11,14 @@ class WakepyMethodTestError(Exception): ... +_class_counter: Counter[str] = Counter() + + def get_new_classname(prefix="TestMethod") -> str: """Creates a new class name. Just to make it easier to generate lots of Methods.""" - if not hasattr(get_new_classname, "counter"): - get_new_classname.counter = 0 - get_new_classname.counter += 1 - return f"{prefix}{get_new_classname.counter}" + _class_counter[prefix] += 1 + return f"{prefix}{_class_counter[prefix]}" _test_method_classes = dict() From cfe6eb6144713839fb96f3ef29407b1be50490d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niko=20F=C3=B6hr?= Date: Sat, 23 Mar 2024 12:20:39 +0200 Subject: [PATCH 54/74] add type annotations for Queues --- tests/integration/dbus_service.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/integration/dbus_service.py b/tests/integration/dbus_service.py index abd47241..3a46e2f9 100644 --- a/tests/integration/dbus_service.py +++ b/tests/integration/dbus_service.py @@ -41,7 +41,7 @@ class DBusService: addr: DBusAddress - def __init__(self, bus_address: str, queue_: queue.Queue, stop: Callable): + def __init__(self, bus_address: str, queue_: queue.Queue[str], stop: Callable): """ Parameters ---------- @@ -158,7 +158,7 @@ def start_dbus_service( --print-address. If not given, uses the service_cls.addr.bus. """ - queue_ = queue.Queue() + queue_: queue.Queue[str] = queue.Queue() should_stop = False def start_service( From 110253039a540e9ca981cda01f82d67dff63d843 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niko=20F=C3=B6hr?= Date: Sat, 23 Mar 2024 12:22:23 +0200 Subject: [PATCH 55/74] add type hints for should_stop Callable --- tests/integration/dbus_service.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/integration/dbus_service.py b/tests/integration/dbus_service.py index 3a46e2f9..e1da76b7 100644 --- a/tests/integration/dbus_service.py +++ b/tests/integration/dbus_service.py @@ -41,7 +41,9 @@ class DBusService: addr: DBusAddress - def __init__(self, bus_address: str, queue_: queue.Queue[str], stop: Callable): + def __init__( + self, bus_address: str, queue_: queue.Queue[str], stop: Callable[[], bool] + ): """ Parameters ---------- @@ -162,7 +164,7 @@ def start_dbus_service( should_stop = False def start_service( - service: Type[DBusService], queue_: queue.Queue, should_stop: Callable + service: Type[DBusService], queue_: queue.Queue, should_stop: Callable[[], bool] ): logger.info(f"Launching dbus service: {service.addr.service}") From cd3f15de2bafbb3d817804f029ee68b735d996db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niko=20F=C3=B6hr?= Date: Sat, 23 Mar 2024 12:22:52 +0200 Subject: [PATCH 56/74] add type hint for Queue --- tests/integration/dbus_service.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/integration/dbus_service.py b/tests/integration/dbus_service.py index e1da76b7..7cc06582 100644 --- a/tests/integration/dbus_service.py +++ b/tests/integration/dbus_service.py @@ -164,7 +164,9 @@ def start_dbus_service( should_stop = False def start_service( - service: Type[DBusService], queue_: queue.Queue, should_stop: Callable[[], bool] + service: Type[DBusService], + queue_: queue.Queue[str], + should_stop: Callable[[], bool], ): logger.info(f"Launching dbus service: {service.addr.service}") From 876c2ce6f1e1d8e946c38d2ece22b117a8d3aafa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niko=20F=C3=B6hr?= Date: Sat, 23 Mar 2024 12:26:42 +0200 Subject: [PATCH 57/74] fix type annotation of Tuple returned in DbusService (tests) --- tests/integration/dbus_service.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/integration/dbus_service.py b/tests/integration/dbus_service.py index 7cc06582..51f7f040 100644 --- a/tests/integration/dbus_service.py +++ b/tests/integration/dbus_service.py @@ -4,7 +4,7 @@ import queue import threading import time -from typing import Callable, Optional, Tuple, Type +from typing import Any, Callable, Optional, Tuple, Type from jeepney import HeaderFields, MessageType, new_error, new_method_return from jeepney.bus_messages import message_bus @@ -141,7 +141,9 @@ def _get_error_message(self, msg, method=".Error.NoMethod"): """Create an error message for replying to a message""" return new_error(msg, self.bus_name + method) - def handle_method(self, method: str, args: Tuple) -> Optional[Tuple[str, Tuple]]: + def handle_method( + self, method: str, args: Tuple + ) -> Optional[Tuple[str, Tuple[Any, ...]]]: """Should return either None (when method does not exist), or tuple of output signature (like "ii" or "sus", etc.), and output values which are of the type defined by the output signature From 8718d9893f8051319b88f7d7fe5d17e198d60fcd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niko=20F=C3=B6hr?= Date: Sat, 23 Mar 2024 12:29:28 +0200 Subject: [PATCH 58/74] mypy fix: Tuple in DbusService.handle_method --- tests/integration/dbus_service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/dbus_service.py b/tests/integration/dbus_service.py index 51f7f040..0a84ac52 100644 --- a/tests/integration/dbus_service.py +++ b/tests/integration/dbus_service.py @@ -142,7 +142,7 @@ def _get_error_message(self, msg, method=".Error.NoMethod"): return new_error(msg, self.bus_name + method) def handle_method( - self, method: str, args: Tuple + self, method: str, args: Tuple[Any, ...] ) -> Optional[Tuple[str, Tuple[Any, ...]]]: """Should return either None (when method does not exist), or tuple of output signature (like "ii" or "sus", etc.), and output values which From 0c5e3665948101f24215495df0ab8a6f189fe3ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niko=20F=C3=B6hr?= Date: Sat, 23 Mar 2024 12:32:08 +0200 Subject: [PATCH 59/74] mypy fix: DbusService start_service dbus address --- tests/integration/dbus_service.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/integration/dbus_service.py b/tests/integration/dbus_service.py index 0a84ac52..c5e15e14 100644 --- a/tests/integration/dbus_service.py +++ b/tests/integration/dbus_service.py @@ -171,8 +171,8 @@ def start_service( should_stop: Callable[[], bool], ): logger.info(f"Launching dbus service: {service.addr.service}") - - service_ = service(bus_address or service.addr.bus, queue_, stop=should_stop) + addr = bus_address or service.addr.bus or "SESSION" + service_ = service(addr, queue_, stop=should_stop) service_.start( server_name=service.addr.service, object_path=service.addr.path, From 780f41cb53575794249340e2ab248eab8b8e5d3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niko=20F=C3=B6hr?= Date: Sat, 23 Mar 2024 12:34:28 +0200 Subject: [PATCH 60/74] mypy: fix errors in tasks.py --- tasks.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/tasks.py b/tasks.py index acb30337..222932e9 100644 --- a/tasks.py +++ b/tasks.py @@ -34,13 +34,14 @@ def get_run_with_print(c): def run_with_print(cmd: str, ignore_errors: bool = False) -> Result: print("Running:", Fore.YELLOW, cmd, Fore.RESET) - return c.run(cmd, pty=platform.system() == "Linux", warn=ignore_errors) + res: Result = c.run(cmd, pty=platform.system() == "Linux", warn=ignore_errors) + return res return run_with_print @task -def format(c): +def format(c) -> None: run = get_run_with_print(c) run("python -m isort .") run("python -m black .") @@ -48,7 +49,7 @@ def format(c): @task -def check(c) -> int: +def check(c) -> None: run = get_run_with_print(c) run("python -m isort --check .") run("python -m black --check .") @@ -57,7 +58,7 @@ def check(c) -> int: @task -def docs(c): +def docs(c) -> None: """Starts sphinx build with live-reload on browser.""" run = get_run_with_print(c) @@ -68,7 +69,7 @@ def docs(c): @task -def test(c, pdb: bool = False): +def test(c, pdb: bool = False) -> None: run = get_run_with_print(c) pdb_flag = " --pdb " if pdb else "" res = run( From 66e94dab34ddbfebb55ce453200b02631819d9cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niko=20F=C3=B6hr?= Date: Sat, 23 Mar 2024 12:35:21 +0200 Subject: [PATCH 61/74] mypy fix: a list in conf.py --- docs/source/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index c3467870..b1bd7195 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -69,7 +69,7 @@ # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This pattern also affects html_static_path and html_extra_path. -exclude_patterns = [] +exclude_patterns: list[str] = [] html_static_path = ["_static"] From 2b74c961830254694f5eab699d9f2826a4dad7fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niko=20F=C3=B6hr?= Date: Sat, 23 Mar 2024 12:46:21 +0200 Subject: [PATCH 62/74] make corresponding *Value for each StrEnum --- tests/unit/test_core/test_constants.py | 21 +++++++++++++++++++++ tests/unit/test_core/test_method.py | 9 ++++++++- wakepy/core/constants.py | 11 +++++++++++ wakepy/core/method.py | 3 +++ 4 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 tests/unit/test_core/test_constants.py diff --git a/tests/unit/test_core/test_constants.py b/tests/unit/test_core/test_constants.py new file mode 100644 index 00000000..c9b1011b --- /dev/null +++ b/tests/unit/test_core/test_constants.py @@ -0,0 +1,21 @@ +import typing + +from wakepy.core import BusType, ModeName, PlatformName +from wakepy.core.constants import BusTypeValue, ModeNameValue, PlatformNameValue + + +def test_platformname(): + """Tests that PlatformNameValue is in synch with PlatformName""" + assert set(typing.get_args(PlatformNameValue)) == { + member.value for member in PlatformName + } + + +def test_modename(): + """Tests that ModeNameValue is in synch with ModeName""" + assert set(typing.get_args(ModeNameValue)) == {member.value for member in ModeName} + + +def test_bustype(): + """Tests that BusTypeValue is in synch with BusType""" + assert set(typing.get_args(BusTypeValue)) == {member.value for member in BusType} diff --git a/tests/unit/test_core/test_method.py b/tests/unit/test_core/test_method.py index 7405b9a8..da76e78c 100644 --- a/tests/unit/test_core/test_method.py +++ b/tests/unit/test_core/test_method.py @@ -6,7 +6,7 @@ import pytest from wakepy.core import DBusMethodCall -from wakepy.core.method import Method, select_methods +from wakepy.core.method import Method, MethodOutcome, MethodOutcomeValue, select_methods from wakepy.core.registry import MethodRegistryError, get_method, get_methods if typing.TYPE_CHECKING: @@ -188,3 +188,10 @@ def test_process_dbus_call(dbus_method: DBusMethod): match=".*cannot process dbus method call.*as it does not have a DBusAdapter", ): assert method.process_dbus_call(DBusMethodCall(dbus_method)) + + +def test_methodoutcome(): + """Tests that MethodOutcomeValue is in synch with MethodOutcome""" + assert set(typing.get_args(MethodOutcomeValue)) == { + member.value for member in MethodOutcome + } diff --git a/wakepy/core/constants.py b/wakepy/core/constants.py index 5e353cce..2dfb80df 100644 --- a/wakepy/core/constants.py +++ b/wakepy/core/constants.py @@ -1,5 +1,7 @@ """Common terms and definitions used in many places""" +import typing + from .strenum import StrEnum, auto @@ -10,6 +12,9 @@ class PlatformName(StrEnum): OTHER = auto() +PlatformNameValue = typing.Literal["WINDOWS", "LINUX", "MACOS", "OTHER"] + + class ModeName(StrEnum): """The names of the modes wakepy supports @@ -20,8 +25,14 @@ class ModeName(StrEnum): KEEP_PRESENTING = "keep.presenting" +ModeNameValue = typing.Literal["keep.running", "keep.presenting"] + + class BusType(StrEnum): """Type of D-Bus bus.""" SESSION = auto() SYSTEM = auto() + + +BusTypeValue = typing.Literal["SESSION", "SYSTEM"] diff --git a/wakepy/core/method.py b/wakepy/core/method.py index fd7ccc21..1ad756d6 100644 --- a/wakepy/core/method.py +++ b/wakepy/core/method.py @@ -65,6 +65,9 @@ class MethodOutcome(StrEnum): FAILURE = auto() +MethodOutcomeValue = typing.Literal["NOT_IMPLEMENTED", "SUCCESS", "FAILURE"] + + unnamed = "__unnamed__" """Constant for defining unnamed Method(s)""" From bf818be105055889ff1b7ac9462742a4fcf2dd25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niko=20F=C3=B6hr?= Date: Sat, 23 Mar 2024 12:47:53 +0200 Subject: [PATCH 63/74] mypy fix: get_method mode arg can be any str --- wakepy/core/registry.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/wakepy/core/registry.py b/wakepy/core/registry.py index 1db8e174..60088eff 100644 --- a/wakepy/core/registry.py +++ b/wakepy/core/registry.py @@ -19,7 +19,7 @@ import typing from typing import overload -from .constants import ModeName +from .constants import ModeName, ModeNameValue if typing.TYPE_CHECKING: from typing import ( @@ -80,7 +80,9 @@ def register_method(method_class: Type[Method]): _method_registry.setdefault(method_class.mode, method_dict) -def get_method(method_name: str, mode: Optional[ModeName] = None) -> MethodCls: +def get_method( + method_name: str, mode: Optional[ModeNameValue | str] = None +) -> MethodCls: """Get a Method class based on its name and optionally the mode. Parameters @@ -89,7 +91,7 @@ def get_method(method_name: str, mode: Optional[ModeName] = None) -> MethodCls: The name of the wakepy.Method. The method must be registered which means that the module containing the subclass definition must have been imported. - mode: str | ModeName | None + mode: str | None If the method_name is registered to methods belonging to multiple Modes, you must provide the mode name, to make the selection unambiguous. Typical mode names are "keep.running" and From a0460ebe5127af762f8fdd6f4b281445cf764d3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niko=20F=C3=B6hr?= Date: Sat, 23 Mar 2024 16:24:07 +0200 Subject: [PATCH 64/74] add do_assert() --- tests/conftest.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 tests/conftest.py diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 00000000..004c30f6 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,32 @@ +from typing import Optional + +import pytest + + +@pytest.fixture +def do_assert(): + """Function to be used instead of assert statement.""" + + # Fixes issue with mypy: https://github.com/python/mypy/issues/11969 + # In short, when testing and needing to assert that a variable has certain + # value, and then mutating the value and asserting the value again ( + # against new assumed value), mypy does not handle that case but you'll get + # [unreachable] errors. Using `do_assert(...)` instead of `assert ...` in + # tests fixes this. + + def _do_assert( + expression: bool, + message: Optional[str] = None, + ) -> None: + """Original idea: Nikita Sobolev (safe-assert)[1]. Fixed the return + type to make this usable[2] + + [1] https://github.com/wemake-services/safe-assert/blob/e3ebfe72a910915e227a9f5447a0f7f56d5219e6/safe_assert/__init__.py + [2] https://github.com/wemake-services/safe-assert/pull/131 + """ + if not expression: + if message: + raise AssertionError(message) + raise AssertionError + + return _do_assert From 13cd116a491e468e0d31cbbae109a0a9bf19843c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niko=20F=C3=B6hr?= Date: Sat, 23 Mar 2024 16:26:37 +0200 Subject: [PATCH 65/74] fix mypy unreachable issues --- tests/unit/test_core/test_mode.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/unit/test_core/test_mode.py b/tests/unit/test_core/test_mode.py index 40978a48..3a33a16e 100644 --- a/tests/unit/test_core/test_mode.py +++ b/tests/unit/test_core/test_mode.py @@ -214,7 +214,7 @@ def test_handle_activation_fail_bad_on_fail_value(): ) -def test_modecontroller(monkeypatch): +def test_modecontroller(monkeypatch, do_assert): # Disable fake success here, because we want to use method_cls for the # activation (and not WakepyFakeSuccess) monkeypatch.setenv("WAKEPY_FAKE_SUCCESS", "0") @@ -223,12 +223,12 @@ def test_modecontroller(monkeypatch): controller = ModeController(Mock(spec_set=DBusAdapter)) # When controller was created, it has not active method or heartbeat - assert controller.active_method is None - assert controller.heartbeat is None + do_assert(controller.active_method is None) + do_assert(controller.heartbeat is None) controller.activate([method_cls]) - assert isinstance(controller.active_method, method_cls) - assert isinstance(controller.heartbeat, Heartbeat) + do_assert(isinstance(controller.active_method, method_cls)) + do_assert(isinstance(controller.heartbeat, Heartbeat)) retval = controller.deactivate() assert retval is True From 568d7e3f27b382bfe30091e470acd868c964454c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niko=20F=C3=B6hr?= Date: Sat, 23 Mar 2024 16:36:07 +0200 Subject: [PATCH 66/74] make Mode.activation_result always ActivationResult (disallow being None) --- tests/unit/test_main.py | 3 +-- wakepy/core/mode.py | 6 +++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/tests/unit/test_main.py b/tests/unit/test_main.py index 2b2dce4b..e83486ee 100644 --- a/tests/unit/test_main.py +++ b/tests/unit/test_main.py @@ -1,7 +1,6 @@ """Tests for the __main__ CLI""" import sys -from typing import Optional from unittest.mock import MagicMock, Mock, call, patch import pytest @@ -160,7 +159,7 @@ def get_mocks_for_main( class TestMode(Mode): active = mode_works - activation_result: Optional[ActivationResult] = None + activation_result: ActivationResult = ActivationResult() mockresult = MagicMock(spec_set=ActivationResult) mockresult.success = mode_works diff --git a/wakepy/core/mode.py b/wakepy/core/mode.py index c32532cd..febd9339 100644 --- a/wakepy/core/mode.py +++ b/wakepy/core/mode.py @@ -160,9 +160,9 @@ class Mode: """True if the mode is active. Otherwise, False. """ - activation_result: ActivationResult | None + activation_result: ActivationResult """The activation result which tells more about the activation process - outcome. None if Mode has not yet been activated. + outcome. """ name: str | None @@ -201,7 +201,7 @@ def __init__( self.method_classes = methods self.methods_priority = methods_priority self.controller: ModeController | None = None - self.activation_result: ActivationResult | None = None + self.activation_result = ActivationResult() self.active: bool = False self.on_fail = on_fail self._dbus_adapter_cls = dbus_adapter From c95399465af6204e934033cbcbf349ec977cc4d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niko=20F=C3=B6hr?= Date: Sat, 23 Mar 2024 16:44:31 +0200 Subject: [PATCH 67/74] run mypy for whole repo, not just ./wakepy --- tasks.py | 2 +- tox.ini | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tasks.py b/tasks.py index 222932e9..239aaf35 100644 --- a/tasks.py +++ b/tasks.py @@ -54,7 +54,7 @@ def check(c) -> None: run("python -m isort --check .") run("python -m black --check .") run("python -m ruff check --no-fix .") - run("python -m mypy ./wakepy") + run("python -m .") @task diff --git a/tox.ini b/tox.ini index e13c7588..b4f50b2b 100644 --- a/tox.ini +++ b/tox.ini @@ -43,7 +43,7 @@ commands = python -m isort . --check --diff python -m black . --check python -m ruff check --no-fix . - python -m mypy ./wakepy + python -m mypy . [testenv:builddocs] description = Build documentation From bd7d5d7bacec7a7a5452ec02ce97f68aa33d3208 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niko=20F=C3=B6hr?= Date: Sat, 23 Mar 2024 18:39:33 +0200 Subject: [PATCH 68/74] add test for StrEnum.name --- tests/unit/test_core/test_strenum.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/unit/test_core/test_strenum.py b/tests/unit/test_core/test_strenum.py index 171d4a70..d68d0846 100644 --- a/tests/unit/test_core/test_strenum.py +++ b/tests/unit/test_core/test_strenum.py @@ -13,7 +13,8 @@ class MyConst(StrEnum): # added as a string assert MyConst.FOO == "fooval" assert isinstance(MyConst.FOO, str) - + assert MyConst.FOO.name == "FOO" + assert MyConst.FOO.value == "fooval" # Test containement # Values can be querid with in operator assert "fooval" in MyConst From b58461a898477c56f2cc851f2325966704ebc157 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niko=20F=C3=B6hr?= Date: Sat, 23 Mar 2024 19:32:00 +0200 Subject: [PATCH 69/74] add pytest to tox check requirements --- pyproject.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 6d3df086..aa9d6802 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -68,6 +68,9 @@ test =[ ] # For linters, code analysis and formatting tools check = [ + # some version of pytest required for checking types. Perhaps any recent + # version of pytest will do. + "pytest", "black==24.2.0", "mypy==1.9.0", "isort==5.13.2", From 6dc29d0df1f0836707da9e114e2ba81f34caebf9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niko=20F=C3=B6hr?= Date: Sat, 23 Mar 2024 21:56:57 +0200 Subject: [PATCH 70/74] add required packages for 'tox check' step --- pyproject.toml | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index aa9d6802..67c2cbc0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,7 +39,7 @@ dev = [ "invoke==2.2.0", # Colorama is used with the tasks.py (invoke commands) "colorama==0.4.6", - "types-colorama", # for mypy + ] # For building documentation doc = [ @@ -68,13 +68,16 @@ test =[ ] # For linters, code analysis and formatting tools check = [ - # some version of pytest required for checking types. Perhaps any recent - # version of pytest will do. - "pytest", + "invoke==2.2.0", "black==24.2.0", "mypy==1.9.0", "isort==5.13.2", "ruff==0.3.2", + # some version of pytest required for checking types. Perhaps any recent + # version of pytest will do. + "pytest", + "types-colorama", # for mypy + "time-machine==2.14.0" # for mypy ] From 67f670280999bb8ad374310ff4e180b79e1390de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niko=20F=C3=B6hr?= Date: Sat, 23 Mar 2024 22:15:47 +0200 Subject: [PATCH 71/74] add Python 3.7 typing support (Literal, get_args) using typing-extensions --- pyproject.toml | 5 ++++- .../test_core/test_activation/test_activation.py | 7 ++++++- tests/unit/test_core/test_constants.py | 7 ++++++- tests/unit/test_core/test_method.py | 7 ++++++- wakepy/core/activation.py | 10 +++++++--- wakepy/core/constants.py | 13 +++++++++---- wakepy/core/method.py | 8 +++++++- 7 files changed, 45 insertions(+), 12 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 67c2cbc0..f110fc95 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,10 @@ authors = [ ] dependencies = [ # For using the D-Bus based methods - "jeepney >= 0.7.1;sys_platform=='linux'" + "jeepney >= 0.7.1;sys_platform=='linux'", + # The typing.Literal was introduced in Python 3.8. Need to install + # typing-extensions for Python 3.7 support. + 'typing-extensions; python_version < "3.8.0"', ] # Python 3.7 introduces from __future__ import annotations diff --git a/tests/unit/test_core/test_activation/test_activation.py b/tests/unit/test_core/test_activation/test_activation.py index a8653824..b2bbde6d 100644 --- a/tests/unit/test_core/test_activation/test_activation.py +++ b/tests/unit/test_core/test_activation/test_activation.py @@ -6,7 +6,7 @@ import datetime as dt import os import re -import typing +import sys from contextlib import contextmanager from unittest.mock import Mock @@ -42,6 +42,11 @@ from wakepy.core.heartbeat import Heartbeat from wakepy.core.method import MethodError +if sys.version_info < (3, 8): + import typing_extensions as typing +else: + import typing + def test_activate_without_methods(monkeypatch): _arrange_for_test_activate(monkeypatch) diff --git a/tests/unit/test_core/test_constants.py b/tests/unit/test_core/test_constants.py index c9b1011b..6491acae 100644 --- a/tests/unit/test_core/test_constants.py +++ b/tests/unit/test_core/test_constants.py @@ -1,8 +1,13 @@ -import typing +import sys from wakepy.core import BusType, ModeName, PlatformName from wakepy.core.constants import BusTypeValue, ModeNameValue, PlatformNameValue +if sys.version_info < (3, 8): + import typing_extensions as typing +else: + import typing + def test_platformname(): """Tests that PlatformNameValue is in synch with PlatformName""" diff --git a/tests/unit/test_core/test_method.py b/tests/unit/test_core/test_method.py index da76e78c..afeeca14 100644 --- a/tests/unit/test_core/test_method.py +++ b/tests/unit/test_core/test_method.py @@ -1,7 +1,7 @@ from __future__ import annotations import re -import typing +import sys import pytest @@ -9,6 +9,11 @@ from wakepy.core.method import Method, MethodOutcome, MethodOutcomeValue, select_methods from wakepy.core.registry import MethodRegistryError, get_method, get_methods +if sys.version_info < (3, 8): + import typing_extensions as typing +else: + import typing + if typing.TYPE_CHECKING: from wakepy.core import DBusMethod diff --git a/wakepy/core/activation.py b/wakepy/core/activation.py index 962340bd..0464b92a 100644 --- a/wakepy/core/activation.py +++ b/wakepy/core/activation.py @@ -21,6 +21,7 @@ from __future__ import annotations import datetime as dt +import sys import typing from dataclasses import dataclass from typing import List, Sequence, Set, Union @@ -32,6 +33,11 @@ from .platform import CURRENT_PLATFORM from .strenum import StrEnum, auto +if sys.version_info < (3, 8): + from typing_extensions import Literal +else: + from typing import Literal + if typing.TYPE_CHECKING: from typing import Optional, Tuple, Type @@ -54,9 +60,7 @@ class StageName(StrEnum): ACTIVATION = auto() -StageNameValue = typing.Literal[ - "NONE", "PLATFORM_SUPPORT", "REQUIREMENTS", "ACTIVATION" -] +StageNameValue = Literal["NONE", "PLATFORM_SUPPORT", "REQUIREMENTS", "ACTIVATION"] class ActivationResult: diff --git a/wakepy/core/constants.py b/wakepy/core/constants.py index 2dfb80df..15389972 100644 --- a/wakepy/core/constants.py +++ b/wakepy/core/constants.py @@ -1,9 +1,14 @@ """Common terms and definitions used in many places""" -import typing +import sys from .strenum import StrEnum, auto +if sys.version_info < (3, 8): + from typing_extensions import Literal +else: + from typing import Literal + class PlatformName(StrEnum): WINDOWS = auto() @@ -12,7 +17,7 @@ class PlatformName(StrEnum): OTHER = auto() -PlatformNameValue = typing.Literal["WINDOWS", "LINUX", "MACOS", "OTHER"] +PlatformNameValue = Literal["WINDOWS", "LINUX", "MACOS", "OTHER"] class ModeName(StrEnum): @@ -25,7 +30,7 @@ class ModeName(StrEnum): KEEP_PRESENTING = "keep.presenting" -ModeNameValue = typing.Literal["keep.running", "keep.presenting"] +ModeNameValue = Literal["keep.running", "keep.presenting"] class BusType(StrEnum): @@ -35,4 +40,4 @@ class BusType(StrEnum): SYSTEM = auto() -BusTypeValue = typing.Literal["SESSION", "SYSTEM"] +BusTypeValue = Literal["SESSION", "SYSTEM"] diff --git a/wakepy/core/method.py b/wakepy/core/method.py index 1ad756d6..733c3c77 100644 --- a/wakepy/core/method.py +++ b/wakepy/core/method.py @@ -14,6 +14,7 @@ from __future__ import annotations +import sys import typing from abc import ABC, ABCMeta from typing import Any, List, Optional, Set, Tuple, Type, TypeVar, Union @@ -25,6 +26,11 @@ if typing.TYPE_CHECKING: from wakepy.core import DBusAdapter, DBusMethodCall +if sys.version_info < (3, 8): + from typing_extensions import Literal +else: + from typing import Literal + MethodCls = Type["Method"] T = TypeVar("T") Collection = Union[List[T], Tuple[T, ...], Set[T]] @@ -65,7 +71,7 @@ class MethodOutcome(StrEnum): FAILURE = auto() -MethodOutcomeValue = typing.Literal["NOT_IMPLEMENTED", "SUCCESS", "FAILURE"] +MethodOutcomeValue = Literal["NOT_IMPLEMENTED", "SUCCESS", "FAILURE"] unnamed = "__unnamed__" From d36e2c457f88d84bc1233acb6572afceb6c77605 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niko=20F=C3=B6hr?= Date: Sat, 23 Mar 2024 22:33:33 +0200 Subject: [PATCH 72/74] refactor: assert_strenum_values Less places where we have to check for python version, and less places where a custom '# pragma: ' is required --- tests/conftest.py | 19 +++++++++++++++ .../test_activation/test_activation.py | 12 ++-------- tests/unit/test_core/test_constants.py | 24 +++++-------------- tests/unit/test_core/test_method.py | 7 ++---- 4 files changed, 29 insertions(+), 33 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 004c30f6..30c9e9ec 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,7 +1,15 @@ +import sys from typing import Optional import pytest +from wakepy.core.strenum import StrEnum + +if sys.version_info < (3, 8): + import typing_extensions as typing +else: + import typing + @pytest.fixture def do_assert(): @@ -30,3 +38,14 @@ def _do_assert( raise AssertionError return _do_assert + + +@pytest.fixture +def assert_strenum_values(): + + def _assert_strenum_values(strenum_cls: typing.Type[StrEnum], values: typing.Any): + """Note: `values` is a typing.Literal. Could not find a type annotation + for that""" + assert set(typing.get_args(values)) == {member.value for member in strenum_cls} + + return _assert_strenum_values diff --git a/tests/unit/test_core/test_activation/test_activation.py b/tests/unit/test_core/test_activation/test_activation.py index b2bbde6d..fbe7ef51 100644 --- a/tests/unit/test_core/test_activation/test_activation.py +++ b/tests/unit/test_core/test_activation/test_activation.py @@ -6,7 +6,6 @@ import datetime as dt import os import re -import sys from contextlib import contextmanager from unittest.mock import Mock @@ -42,11 +41,6 @@ from wakepy.core.heartbeat import Heartbeat from wakepy.core.method import MethodError -if sys.version_info < (3, 8): - import typing_extensions as typing -else: - import typing - def test_activate_without_methods(monkeypatch): _arrange_for_test_activate(monkeypatch) @@ -605,13 +599,11 @@ def test_deactivate_fail_heartbeat_not_stopping(): deactivate_method(method, heartbeat) -def test_stagename(): +def test_stagename(assert_strenum_values): assert StageName.PLATFORM_SUPPORT == "PLATFORM_SUPPORT" assert StageName.ACTIVATION == "ACTIVATION" assert StageName.REQUIREMENTS == "REQUIREMENTS" - assert set(typing.get_args(StageNameValue)) == { - member.value for member in StageName - } + assert_strenum_values(StageName, StageNameValue) # These are the only "falsy" values for WAKEPY_FAKE_SUCCESS diff --git a/tests/unit/test_core/test_constants.py b/tests/unit/test_core/test_constants.py index 6491acae..1611f02f 100644 --- a/tests/unit/test_core/test_constants.py +++ b/tests/unit/test_core/test_constants.py @@ -1,26 +1,14 @@ -import sys - from wakepy.core import BusType, ModeName, PlatformName from wakepy.core.constants import BusTypeValue, ModeNameValue, PlatformNameValue -if sys.version_info < (3, 8): - import typing_extensions as typing -else: - import typing - -def test_platformname(): - """Tests that PlatformNameValue is in synch with PlatformName""" - assert set(typing.get_args(PlatformNameValue)) == { - member.value for member in PlatformName - } +def test_platformname(assert_strenum_values): + assert_strenum_values(PlatformName, PlatformNameValue) -def test_modename(): - """Tests that ModeNameValue is in synch with ModeName""" - assert set(typing.get_args(ModeNameValue)) == {member.value for member in ModeName} +def test_modename(assert_strenum_values): + assert_strenum_values(ModeName, ModeNameValue) -def test_bustype(): - """Tests that BusTypeValue is in synch with BusType""" - assert set(typing.get_args(BusTypeValue)) == {member.value for member in BusType} +def test_bustype(assert_strenum_values): + assert_strenum_values(BusType, BusTypeValue) diff --git a/tests/unit/test_core/test_method.py b/tests/unit/test_core/test_method.py index afeeca14..529c43aa 100644 --- a/tests/unit/test_core/test_method.py +++ b/tests/unit/test_core/test_method.py @@ -195,8 +195,5 @@ def test_process_dbus_call(dbus_method: DBusMethod): assert method.process_dbus_call(DBusMethodCall(dbus_method)) -def test_methodoutcome(): - """Tests that MethodOutcomeValue is in synch with MethodOutcome""" - assert set(typing.get_args(MethodOutcomeValue)) == { - member.value for member in MethodOutcome - } +def test_methodoutcome(assert_strenum_values): + assert_strenum_values(MethodOutcome, MethodOutcomeValue) From cf7c129e9d50e20e2192402aecd2a250266b8204 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niko=20F=C3=B6hr?= Date: Sat, 23 Mar 2024 22:47:43 +0200 Subject: [PATCH 73/74] restore 100% test coverage - Added new rules: no-cover-if-py-gte-38 and no-cover-if-py-lt-38 --- pyproject.toml | 2 ++ tests/conftest.py | 4 ++-- tests/unit/test_core/test_method.py | 4 ++-- wakepy/core/activation.py | 4 ++-- wakepy/core/constants.py | 4 ++-- wakepy/core/method.py | 4 ++-- 6 files changed, 12 insertions(+), 10 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index f110fc95..abcb53d0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -139,6 +139,8 @@ exclude_lines = [ [tool.coverage.coverage_conditional_plugin.rules] no-cover-if-no-dbus = "platform_system != 'Linux'" +no-cover-if-py-gte-38 = "sys_version_info >= (3, 8)" +no-cover-if-py-lt-38 = "sys_version_info < (3, 8)" [tool.isort] profile = "black" diff --git a/tests/conftest.py b/tests/conftest.py index 30c9e9ec..53724ebc 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -5,9 +5,9 @@ from wakepy.core.strenum import StrEnum -if sys.version_info < (3, 8): +if sys.version_info < (3, 8): # pragma: no-cover-if-py-gte-38 import typing_extensions as typing -else: +else: # pragma: no-cover-if-py-lt-38 import typing diff --git a/tests/unit/test_core/test_method.py b/tests/unit/test_core/test_method.py index 529c43aa..b1136e4b 100644 --- a/tests/unit/test_core/test_method.py +++ b/tests/unit/test_core/test_method.py @@ -9,9 +9,9 @@ from wakepy.core.method import Method, MethodOutcome, MethodOutcomeValue, select_methods from wakepy.core.registry import MethodRegistryError, get_method, get_methods -if sys.version_info < (3, 8): +if sys.version_info < (3, 8): # pragma: no-cover-if-py-gte-38 import typing_extensions as typing -else: +else: # pragma: no-cover-if-py-lt-38 import typing if typing.TYPE_CHECKING: diff --git a/wakepy/core/activation.py b/wakepy/core/activation.py index 0464b92a..f7a1b13a 100644 --- a/wakepy/core/activation.py +++ b/wakepy/core/activation.py @@ -33,9 +33,9 @@ from .platform import CURRENT_PLATFORM from .strenum import StrEnum, auto -if sys.version_info < (3, 8): +if sys.version_info < (3, 8): # pragma: no-cover-if-py-gte-38 from typing_extensions import Literal -else: +else: # pragma: no-cover-if-py-lt-38 from typing import Literal if typing.TYPE_CHECKING: diff --git a/wakepy/core/constants.py b/wakepy/core/constants.py index 15389972..60682e1d 100644 --- a/wakepy/core/constants.py +++ b/wakepy/core/constants.py @@ -4,9 +4,9 @@ from .strenum import StrEnum, auto -if sys.version_info < (3, 8): +if sys.version_info < (3, 8): # pragma: no-cover-if-py-gte-38 from typing_extensions import Literal -else: +else: # pragma: no-cover-if-py-lt-38 from typing import Literal diff --git a/wakepy/core/method.py b/wakepy/core/method.py index 733c3c77..d15b2ee8 100644 --- a/wakepy/core/method.py +++ b/wakepy/core/method.py @@ -26,9 +26,9 @@ if typing.TYPE_CHECKING: from wakepy.core import DBusAdapter, DBusMethodCall -if sys.version_info < (3, 8): +if sys.version_info < (3, 8): # pragma: no-cover-if-py-gte-38 from typing_extensions import Literal -else: +else: # pragma: no-cover-if-py-lt-38 from typing import Literal MethodCls = Type["Method"] From 0b5315d0ae2a52cc8a2acd45ad3f858cad13a1a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niko=20F=C3=B6hr?= Date: Sat, 23 Mar 2024 22:53:55 +0200 Subject: [PATCH 74/74] add mention about installing typing-extensions --- README.md | 1 + docs/source/index.md | 2 ++ 2 files changed, 3 insertions(+) diff --git a/README.md b/README.md index 96f62678..7fdfdf87 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,7 @@ Wakepy has two main modes: - Permissive MIT licence - Low amount of python dependencies - For using the D-Bus methods on Linux: [jeepney](https://jeepney.readthedocs.io/) + - On Python 3.7: [typing-extensions](https://pypi.org/project/typing-extensions/). - Otherwise: None diff --git a/docs/source/index.md b/docs/source/index.md index 2b1dd3e3..fb7d38de 100644 --- a/docs/source/index.md +++ b/docs/source/index.md @@ -16,6 +16,8 @@ pip install wakepy ```{note} On Linux will install also **[`jeepney`](https://jeepney.readthedocs.io/)** for DBus communication (if not installed). On other systems there are no python requirements. + +On Python 3.7 installs [typing-extensions](https://pypi.org/project/typing-extensions/). ``` ## Basic Usage