diff --git a/.github/workflows/ubuntu_test.yml b/.github/workflows/ubuntu_test.yml index 3cd0950..6a028ea 100644 --- a/.github/workflows/ubuntu_test.yml +++ b/.github/workflows/ubuntu_test.yml @@ -4,6 +4,8 @@ on: push: pull_request: workflow_dispatch: + schedule: + - cron: '0 0 * * 5' jobs: unlimited: diff --git a/.python-version b/.python-version deleted file mode 100644 index 2e40913..0000000 --- a/.python-version +++ /dev/null @@ -1,2 +0,0 @@ -3.8.7 -system diff --git a/README.md b/README.md index 20dc538..da37713 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ -[![CodeQL](https://github.com/python-sdbus/python-sdbus/actions/workflows/codeql.yml/badge.svg)](https://github.com/python-sdbus/python-sdbus/actions/workflows/codeql.yml) [![Documentation Status](https://readthedocs.org/projects/python-sdbus/badge/?version=latest)](https://python-sdbus.readthedocs.io/en/latest/?badge=latest) +[![PyPI - Version](https://img.shields.io/pypi/v/sdbus)](https://pypi.org/project/sdbus/) # Modern Python library for D-Bus diff --git a/docs/autodoc.rst b/docs/autodoc.rst index be35525..21d27ff 100644 --- a/docs/autodoc.rst +++ b/docs/autodoc.rst @@ -12,7 +12,7 @@ To use it include ``"sdbus.autodoc"`` extension in your extensions = ['sdbus.autodoc'] The extension can document interface class bodies. For example, -`python-sdbus-networkmanager `_ +`python-sdbus-networkmanager `_ uses it to document the classes. .. code-block:: rst diff --git a/setup.py b/setup.py index a4fed7c..bee9877 100644 --- a/setup.py +++ b/setup.py @@ -97,15 +97,15 @@ def get_link_arguments() -> list[str]: long_description=long_description, long_description_content_type='text/markdown', version='0.14.0', - url='https://github.com/igo95862/python-sdbus', + url='https://github.com/python-sdbus/python-sdbus', author='igo95862', author_email='igo95862@yandex.ru', license='LGPL-2.1-or-later', keywords='dbus ipc linux freedesktop', project_urls={ 'Documentation': 'https://python-sdbus.readthedocs.io/en/latest/', - 'Source': 'https://github.com/igo95862/python-sdbus/', - 'Tracker': 'https://github.com/igo95862/python-sdbus/issues/', + 'Source': 'https://github.com/python-sdbus/python-sdbus/', + 'Tracker': 'https://github.com/python-sdbus/python-sdbus/issues/', }, classifiers=[ 'Development Status :: 4 - Beta', diff --git a/src/sdbus/dbus_exceptions.py b/src/sdbus/dbus_exceptions.py index eb78b99..b178503 100644 --- a/src/sdbus/dbus_exceptions.py +++ b/src/sdbus/dbus_exceptions.py @@ -38,7 +38,7 @@ def __new__( name: str, bases: tuple[type, ...], namespace: dict[str, Any], - ) -> DbusErrorMeta: + ) -> type[Exception]: dbus_error_name = namespace.get('dbus_error_name') diff --git a/src/sdbus/dbus_proxy_async_interface_base.py b/src/sdbus/dbus_proxy_async_interface_base.py index e0b6f5f..6fb927e 100644 --- a/src/sdbus/dbus_proxy_async_interface_base.py +++ b/src/sdbus/dbus_proxy_async_interface_base.py @@ -22,7 +22,6 @@ from collections.abc import Callable from copy import copy from itertools import chain -from types import MethodType from typing import TYPE_CHECKING, Any, cast from warnings import warn from weakref import WeakKeyDictionary, WeakValueDictionary @@ -73,21 +72,23 @@ def _process_dbus_method_override( mro_dbus_elements: dict[str, DbusMemberAsync], ) -> DbusMethodAsync: try: - original_method = mro_dbus_elements[override_attr_name] + original_dbus_method = mro_dbus_elements[override_attr_name] except KeyError: raise ValueError( f"No D-Bus method {override_attr_name!r} found " f"to override." ) - if not isinstance(original_method, DbusMethodAsync): + if not isinstance(original_dbus_method, DbusMethodAsync): raise TypeError( - f"Expected {DbusMethodAsync!r} got {original_method!r} " + f"Expected {DbusMethodAsync!r} got {original_dbus_method!r} " f"under name {override_attr_name!r}" ) - new_method = copy(original_method) - new_method.original_method = cast(MethodType, override.override_method) + new_method = copy(original_dbus_method) + new_method.original_method = ( + override.override_method # type: ignore[assignment] + ) return new_method @staticmethod @@ -294,7 +295,7 @@ def _dbus_iter_interfaces_meta( cls, ) -> Iterator[tuple[str, DbusClassMeta]]: - for base in cls.__mro__: + for base in reversed(cls.__mro__): meta = DBUS_CLASS_TO_META.get(base) if meta is None: continue diff --git a/src/sdbus/dbus_proxy_async_interfaces.py b/src/sdbus/dbus_proxy_async_interfaces.py index 7964b0b..303aaff 100644 --- a/src/sdbus/dbus_proxy_async_interfaces.py +++ b/src/sdbus/dbus_proxy_async_interfaces.py @@ -105,6 +105,7 @@ async def properties_get_all_dict( class DbusInterfaceCommonAsync( - DbusPeerInterfaceAsync, DbusPropertiesInterfaceAsync, - DbusIntrospectableAsync): + DbusPropertiesInterfaceAsync, + DbusIntrospectableAsync, + DbusPeerInterfaceAsync): ... diff --git a/src/sdbus/dbus_proxy_async_object_manager.py b/src/sdbus/dbus_proxy_async_object_manager.py index d252662..06d636e 100644 --- a/src/sdbus/dbus_proxy_async_object_manager.py +++ b/src/sdbus/dbus_proxy_async_object_manager.py @@ -49,8 +49,8 @@ def __init__( self.remove_object_call = remove_object_call def stop(self) -> None: - super().stop() self.remove_object_call() + super().stop() class DbusObjectManagerInterfaceAsync( diff --git a/src/sdbus/dbus_proxy_sync_interface_base.py b/src/sdbus/dbus_proxy_sync_interface_base.py index dfd3bda..6358c74 100644 --- a/src/sdbus/dbus_proxy_sync_interface_base.py +++ b/src/sdbus/dbus_proxy_sync_interface_base.py @@ -171,7 +171,7 @@ def _dbus_iter_interfaces_meta( cls, ) -> Iterator[tuple[str, DbusClassMeta]]: - for base in cls.__mro__: + for base in reversed(cls.__mro__): meta = DBUS_CLASS_TO_META.get(base) if meta is None: continue diff --git a/src/sdbus/dbus_proxy_sync_interfaces.py b/src/sdbus/dbus_proxy_sync_interfaces.py index 90cc21f..b14e73b 100644 --- a/src/sdbus/dbus_proxy_sync_interfaces.py +++ b/src/sdbus/dbus_proxy_sync_interfaces.py @@ -94,9 +94,9 @@ def properties_get_all_dict( class DbusInterfaceCommon( - DbusPeerInterface, + DbusPropertiesInterface, DbusIntrospectable, - DbusPropertiesInterface): + DbusPeerInterface): ... diff --git a/src/sdbus/sd_bus_internals.py b/src/sdbus/sd_bus_internals.py index 1f3a0a0..54099d4 100644 --- a/src/sdbus/sd_bus_internals.py +++ b/src/sdbus/sd_bus_internals.py @@ -256,7 +256,7 @@ def map_exception_to_dbus_error(exc: type[Exception], ... # We want to be able to generate docs without module -def add_exception_mapping(exc: Exception, /) -> None: +def add_exception_mapping(exc: type[Exception], /) -> None: ... # We want to be able to generate docs without module diff --git a/src/sdbus/sd_bus_internals_bus.c b/src/sdbus/sd_bus_internals_bus.c index f66a14d..ca7b817 100644 --- a/src/sdbus/sd_bus_internals_bus.c +++ b/src/sdbus/sd_bus_internals_bus.c @@ -168,22 +168,10 @@ static SdBusMessageObject* SdBus_new_signal_message(SdBusObject* self, PyObject* return new_message_object; } -#ifndef Py_LIMITED_API -static int _check_sdbus_message(PyObject* something) { - return PyType_IsSubtype(Py_TYPE(something), (PyTypeObject*)SdBusMessage_class); -} - -static SdBusMessageObject* SdBus_call(SdBusObject* self, PyObject* const* args, Py_ssize_t nargs) { - // TODO: Check reference counting - SD_BUS_PY_CHECK_ARGS_NUMBER(1); - SD_BUS_PY_CHECK_ARG_CHECK_FUNC(0, _check_sdbus_message); - - SdBusMessageObject* call_message = (SdBusMessageObject*)args[0]; -#else -static SdBusMessageObject* SdBus_call(SdBusObject* self, PyObject* args) { +static SdBusMessageObject* SdBus_call(SdBusObject* self, PyObject* arg) { SdBusMessageObject* call_message = NULL; - CALL_PYTHON_BOOL_CHECK(PyArg_ParseTuple(args, "O", &call_message, NULL)); -#endif + CALL_PYTHON_BOOL_CHECK(PyArg_Parse(arg, "O!", SdBusMessage_class, &call_message, NULL)); + SdBusMessageObject* reply_message_object CLEANUP_SD_BUS_MESSAGE = (SdBusMessageObject*)CALL_PYTHON_AND_CHECK(SD_BUS_PY_CLASS_DUNDER_NEW(SdBusMessage_class)); @@ -316,17 +304,10 @@ int SdBus_async_callback(sd_bus_message* m, return 0; } -#ifndef Py_LIMITED_API -static PyObject* SdBus_call_async(SdBusObject* self, PyObject* const* args, Py_ssize_t nargs) { - SD_BUS_PY_CHECK_ARGS_NUMBER(1); - SD_BUS_PY_CHECK_ARG_CHECK_FUNC(0, _check_sdbus_message); - - SdBusMessageObject* call_message = (SdBusMessageObject*)args[0]; -#else -static PyObject* SdBus_call_async(SdBusObject* self, PyObject* args) { +static PyObject* SdBus_call_async(SdBusObject* self, PyObject* arg) { SdBusMessageObject* call_message = NULL; - CALL_PYTHON_BOOL_CHECK(PyArg_ParseTuple(args, "O", &call_message, NULL)); -#endif + CALL_PYTHON_BOOL_CHECK(PyArg_Parse(arg, "O!", SdBusMessage_class, &call_message, NULL)); + PyObject* running_loop = CALL_PYTHON_AND_CHECK(_get_or_bind_loop(self)); PyObject* new_future = CALL_PYTHON_AND_CHECK(PyObject_CallMethod(running_loop, "create_future", "")); @@ -717,8 +698,8 @@ static PyObject* SdBus_asyncio_update_fd_watchers(SdBusObject* self) { } static PyMethodDef SdBus_methods[] = { - {"call", (SD_BUS_PY_FUNC_TYPE)SdBus_call, SD_BUS_PY_METH, PyDoc_STR("Send message and block until the reply.")}, - {"call_async", (SD_BUS_PY_FUNC_TYPE)SdBus_call_async, SD_BUS_PY_METH, PyDoc_STR("Async send message, returns awaitable future.")}, + {"call", (PyCFunction)SdBus_call, METH_O, PyDoc_STR("Send message and block until the reply.")}, + {"call_async", (PyCFunction)SdBus_call_async, METH_O, PyDoc_STR("Async send message, returns awaitable future.")}, {"process", (PyCFunction)SdBus_process, METH_NOARGS, PyDoc_STR("Process pending IO work.")}, {"get_fd", (SD_BUS_PY_FUNC_TYPE)SdBus_get_fd, SD_BUS_PY_METH, PyDoc_STR("Get file descriptor to poll on.")}, {"new_method_call_message", (SD_BUS_PY_FUNC_TYPE)SdBus_new_method_call_message, SD_BUS_PY_METH, PyDoc_STR("Create new empty method call message.")}, diff --git a/test/benchmarks/bench_block_ping.py b/test/benchmarks/bench_block_ping.py new file mode 100644 index 0000000..36c68d6 --- /dev/null +++ b/test/benchmarks/bench_block_ping.py @@ -0,0 +1,52 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later + +# Copyright (C) 2025 igo95862 + +# This file is part of python-sdbus + +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. + +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. + +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +from __future__ import annotations + +from time import perf_counter + +import pyperf # type: ignore +from sdbus.unittest import _isolated_dbus + +from sdbus import DbusInterfaceCommon + + +def bench_block_ping(loops: int) -> float: + with _isolated_dbus() as bus: + dbus_interface = DbusInterfaceCommon( + "org.freedesktop.DBus", + "/org/freedesktop/DBus", + bus, + ) + + start = perf_counter() + + for _ in range(loops): + dbus_interface.dbus_ping() + + return perf_counter() - start + + +def main() -> None: + runner = pyperf.Runner() + runner.bench_time_func('sdbus_block_ping', bench_block_ping) + + +if __name__ == "__main__": + main() diff --git a/test/test_object_manager.py b/test/test_object_manager.py index 541c28a..2c01a9a 100644 --- a/test/test_object_manager.py +++ b/test/test_object_manager.py @@ -399,7 +399,10 @@ async def test_secondary_export_handle(self) -> None: ) self.assertEqual(added.output[0][0], MANAGED_PATH) - self.assertEqual(removed.output[0][0], MANAGED_PATH) + + removed_path, removed_interfaces = removed.output[0] + self.assertEqual(removed_path, MANAGED_PATH) + self.assertIn(MANAGED_INTERFACE_NAME, removed_interfaces) with self.assertRaises(DbusUnknownObjectError): await managed_proxy.test_int diff --git a/test/test_sdbus_async.py b/test/test_sdbus_async.py index ccf341c..7e1d885 100644 --- a/test/test_sdbus_async.py +++ b/test/test_sdbus_async.py @@ -948,9 +948,23 @@ class TwoInterface( async def two(self) -> int: return 2 - class CombinedInterface(OneInterface, TwoInterface): + class CombinedInterface(TwoInterface, OneInterface): ... + test_combined = CombinedInterface() + test_combined_interfaces = [ + iface for iface, _ in test_combined._dbus_iter_interfaces_meta() + ] + + # Verify the order of reported interfaces on the combined class. + self.assertEqual(test_combined_interfaces, [ + "org.freedesktop.DBus.Peer", + "org.freedesktop.DBus.Introspectable", + "org.freedesktop.DBus.Properties", + "org.example.one", + "org.example.two", + ]) + async def test_extremely_large_string(self) -> None: test_object, test_object_connection = initialize_object() diff --git a/test/test_sdbus_block.py b/test/test_sdbus_block.py index e267f28..ef650d3 100644 --- a/test/test_sdbus_block.py +++ b/test/test_sdbus_block.py @@ -92,9 +92,23 @@ class TwoInterface( def two(self) -> int: raise NotImplementedError - class CombinedInterface(OneInterface, TwoInterface): + class CombinedInterface(TwoInterface, OneInterface): ... + test_combined = CombinedInterface("org.test", "/") + test_combined_interfaces = [ + iface for iface, _ in test_combined._dbus_iter_interfaces_meta() + ] + + # Verify the order of reported interfaces on the combined class. + self.assertEqual(test_combined_interfaces, [ + "org.freedesktop.DBus.Peer", + "org.freedesktop.DBus.Introspectable", + "org.freedesktop.DBus.Properties", + "org.example.one", + "org.example.two", + ]) + if __name__ == '__main__': main() diff --git a/tox.ini b/tox.ini deleted file mode 100644 index 21846b4..0000000 --- a/tox.ini +++ /dev/null @@ -1,6 +0,0 @@ -[tox] -envlist = py38,py39 - -[testenv] -commands = python -m unittest --verbose -