diff --git a/pyproject.toml b/pyproject.toml index aa72cdad..59fa32c2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -104,6 +104,7 @@ exclude_lines = [ "if (?:typing\\.)?TYPE_CHECKING:", "@(abc\\.)?abstractmethod", "@(typing\\.)?overload", + "raise NotImplementedError" ] [tool.coverage.coverage_conditional_plugin.omit] diff --git a/src/wakepy/core/dbus.py b/src/wakepy/core/dbus.py index 5ae66060..06c1a9f6 100644 --- a/src/wakepy/core/dbus.py +++ b/src/wakepy/core/dbus.py @@ -264,7 +264,7 @@ def __repr__(self) -> str: return f"<{self.method.service} {self.args} | bus: {self.method.bus}>" -class DBusAdapter: +class DBusAdapter: # pragma: no-cover-if-no-dbus """Defines the DBusAdapter interface. This is to be subclassed, and each subclass is usually an implementation for a DBusAdapter using single python (dbus-)library. @@ -279,6 +279,10 @@ class DBusAdapter: wakepy (Modes). """ + def __init__(self) -> None: + # The values are DBusConnections. Type is defined by the used library. + self._connections: Dict[Optional[Union[str, BusType]], object] = dict() + def process(self, call: DBusMethodCall) -> object: """Processes a :class:`~wakepy.core.DBusMethodCall`. @@ -290,6 +294,29 @@ def process(self, call: DBusMethodCall) -> object: created within the :func:`~wakepy.DBusAdapter.process` call (this may of course be cached).""" + def _get_connection( + self, bus: Optional[Union[str, BusType]] = BusType.SESSION + ) -> object: + """Gets either a new connection or a cached one, if there is such. + Caching of connections is done on bus level.""" + + if bus in self._connections: + return self._connections[bus] + + connection = self._create_new_connection(bus) + + self._connections[bus] = connection + return connection + + def _create_new_connection( + self, bus: Optional[Union[str, BusType]] = BusType.SESSION + ) -> object: + """Create a new Dbus connection for a bus using the library of choice. + For example, when creating DBusAdapter subclass for jeepney, could + return an instance of ``jeepney.io.blocking.DBusConnection``. + """ + raise NotImplementedError("Implement in subclass") + def get_dbus_adapter( dbus_adapter: Optional[Type[DBusAdapter] | DBusAdapterTypeSeq] = None, diff --git a/src/wakepy/dbus_adapters/jeepney.py b/src/wakepy/dbus_adapters/jeepney.py index 838025a0..b5bf78e8 100644 --- a/src/wakepy/dbus_adapters/jeepney.py +++ b/src/wakepy/dbus_adapters/jeepney.py @@ -1,8 +1,19 @@ +from __future__ import annotations + +import typing +from typing import cast + from jeepney import DBusAddress, new_method_call -from jeepney.io.blocking import open_dbus_connection +from jeepney.io.blocking import DBusConnection, open_dbus_connection from jeepney.wrappers import unwrap_msg from wakepy.core import DBusAdapter, DBusMethodCall +from wakepy.core.dbus import BusType + +if typing.TYPE_CHECKING: + from typing import Optional, Union + + TypeOfBus = Optional[Union[BusType, str]] class DBusNotFoundError(RuntimeError): ... @@ -30,8 +41,17 @@ def process(self, call: DBusMethodCall) -> object: signature=call.method.signature, body=call.args, ) + + connection = cast(DBusConnection, self._get_connection(call.method.bus)) # type: ignore[no-any-unimported] + reply = connection.send_and_get_reply(msg, timeout=self.timeout) + resp = unwrap_msg(reply) + return resp + + def _create_new_connection( # type: ignore[no-any-unimported] + self, bus: TypeOfBus = BusType.SESSION + ) -> DBusConnection: try: - connection = open_dbus_connection(bus=call.method.bus) + return open_dbus_connection(bus=bus) except KeyError as exc: if "DBUS_SESSION_BUS_ADDRESS" in str(exc): raise DBusNotFoundError( @@ -44,6 +64,3 @@ def process(self, call: DBusMethodCall) -> object: ) from exc else: raise - reply = connection.send_and_get_reply(msg, timeout=self.timeout) - resp = unwrap_msg(reply) - return resp diff --git a/tasks.py b/tasks.py index e9e16bf9..9e2980ce 100644 --- a/tasks.py +++ b/tasks.py @@ -86,7 +86,8 @@ def test(c, pdb: bool = False) -> None: run = get_run_with_print(c) pdb_flag = " --pdb " if pdb else "" res = run( - f"python -m pytest {pdb_flag}--cov-branch --cov wakepy --cov-fail-under=100", + f"env -u DBUS_SESSION_BUS_ADDRESS python -m pytest {pdb_flag}--cov-branch " + "--cov wakepy --cov-fail-under=100", ignore_errors=True, ) if res.exited: diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 905f6ef2..083b9d0b 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -6,6 +6,7 @@ import logging import subprocess import sys +import warnings import pytest @@ -32,7 +33,11 @@ def gc_collect_after_dbus_integration_tests(): # this as garbage colletion is triggered also automatically. The garbage # collection must be triggered here manually as the warnings are # ResourceWarning is only filtered away in the dbus integration tests. - gc.collect() + + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + gc.collect() + logger.debug("called gc.collect") diff --git a/tests/integration/test_dbus_adapters.py b/tests/integration/test_dbus_adapters.py index 45f32973..b478f2aa 100644 --- a/tests/integration/test_dbus_adapters.py +++ b/tests/integration/test_dbus_adapters.py @@ -180,3 +180,11 @@ def test_random_keyerror_on_connection_creation( match="Some other reason", ): self.adapter.process(self.call) + + +def test_jeepney_adapter_caching(private_bus: str): + adapter = JeepneyDBusAdapter() + con = adapter._get_connection(private_bus) + + # Call again with same bus name -> get same (cached) connection. + assert adapter._get_connection(private_bus) is con diff --git a/tox.ini b/tox.ini index 699d0ac1..b7c9193e 100644 --- a/tox.ini +++ b/tox.ini @@ -13,8 +13,6 @@ minversion = 4.8.0 [testenv] description = run the tests with pytest deps = -r{toxinidir}/requirements/requirements-test.txt -passenv = - DBUS_SESSION_BUS_ADDRESS commands = ; -W error: turn warnings into errors {envpython} -m pytest -W error {tty:--color=yes} \