From 3f70eafac91203416c94f85b43de310be887f172 Mon Sep 17 00:00:00 2001 From: igo95862 Date: Wed, 14 Sep 2022 21:07:58 +0300 Subject: [PATCH 001/188] Make EmitsChangedSignal introspection more accurate Now has 5 values: None, True, False, 'const', 'invalidates' --- src/sdbus/interface_generator.py | 43 ++++++++++++++++++++++---------- test/test_interface_generator.py | 43 +++++++++++++++++++++++++++++++- 2 files changed, 72 insertions(+), 14 deletions(-) diff --git a/src/sdbus/interface_generator.py b/src/sdbus/interface_generator.py index 273f53c..f5feb08 100644 --- a/src/sdbus/interface_generator.py +++ b/src/sdbus/interface_generator.py @@ -20,7 +20,16 @@ from __future__ import annotations from pathlib import Path -from typing import Dict, Iterable, Iterator, List, Optional, Tuple, Union +from typing import ( + Dict, + Iterable, + Iterator, + List, + Literal, + Optional, + Tuple, + Union, +) from xml.etree.ElementTree import Element from xml.etree.ElementTree import fromstring as etree_from_str from xml.etree.ElementTree import parse as etree_from_file @@ -402,12 +411,12 @@ def __repr__(self) -> str: class DbusPropertyIntrospection(DbusMemberAbstract): - _EMITS_CHANGED_MAP: Dict[str, Optional[str]] = { - 'true': 'DbusPropertyEmitsChangeFlag', - 'false': None, - 'invalidates': 'DbusPropertyEmitsInvalidationFlag', - 'const': 'DbusPropertyConstFlag', - } + _EMITS_CHANGED_MAP: \ + Dict[Union[bool, None, Literal['const', 'invalidates']], str] = { + True: 'DbusPropertyEmitsChangeFlag', + 'invalidates': 'DbusPropertyEmitsInvalidationFlag', + 'const': 'DbusPropertyConstFlag', + } def __init__(self, element: Element): if element.tag != 'property': @@ -415,7 +424,8 @@ def __init__(self, element: Element): self.dbus_signature = element.attrib['type'] - self.emits_changed: Optional[str] = None + self.emits_changed: \ + Union[bool, Literal['const', 'invalidates'], None] = None self.is_explicit = False access_type = element.attrib['access'] @@ -429,8 +439,9 @@ def __init__(self, element: Element): super().__init__(element) def _flags_iter(self) -> Iterator[str]: - if self.emits_changed is not None: - yield self.emits_changed + emits_changed_str = self._EMITS_CHANGED_MAP.get(self.emits_changed) + if emits_changed_str is not None: + yield emits_changed_str yield from super()._flags_iter() @@ -440,11 +451,17 @@ def _parse_annotation_data(self, if annotation_name == ('org.freedesktop.DBus.Property' '.EmitsChangedSignal'): - if annotation_value not in self._EMITS_CHANGED_MAP: + if annotation_value == 'true': + self.emits_changed = True + elif annotation_value == 'false': + self.emits_changed = False + elif annotation_value == 'const': + self.emits_changed = 'const' + elif annotation_value == 'invalidates': + self.emits_changed = 'invalidates' + else: raise ValueError('Unknown EmitsChanged value', annotation_value) - - self.emits_changed = self._EMITS_CHANGED_MAP[annotation_value] elif annotation_name == 'org.freedesktop.systemd1.Explicit': self.is_explicit = parse_str_bool(annotation_value) diff --git a/test/test_interface_generator.py b/test/test_interface_generator.py index f208477..292d85a 100644 --- a/test/test_interface_generator.py +++ b/test/test_interface_generator.py @@ -53,6 +53,18 @@ + + + + + + + + + @@ -141,7 +153,36 @@ def test_parsing(self) -> None: if find_spec('jinja2') is None: raise SkipTest('Jinja2 not installed') - generate_async_py_file(interfaces_from_str(test_xml)) + interfaces_intro = interfaces_from_str(test_xml) + + with self.subTest('Test introspection details'): + test_interface = interfaces_intro[0] + + for test_property in test_interface.properties: + if test_property.method_name == 'BoundBy': + self.assertEqual( + test_property.emits_changed, + 'const', + ) + elif test_property.method_name == 'Bar': + self.assertEqual( + test_property.emits_changed, + None, + ) + elif test_property.method_name == 'FooInvalidates': + self.assertEqual( + test_property.emits_changed, + 'invalidates', + ) + elif test_property.method_name == 'FooFoo': + self.assertEqual( + test_property.emits_changed, + False, + ) + + generated = generate_async_py_file(interfaces_intro) + self.assertIn('flags=DbusPropertyEmitsInvalidationFlag', generated) + self.assertIn('flags=DbusPropertyConstFlag', generated) if __name__ == "__main__": From 938ca81a8734ec81e86c61f6c46dce741507453b Mon Sep 17 00:00:00 2001 From: igo95862 Date: Thu, 15 Sep 2022 23:16:36 +0300 Subject: [PATCH 002/188] Add support for empty signals Meaning signals without data. Declare them with functions that return None and have dbus signature "". ``` @dbus_signal_async() def empty_signal(self) -> None: raise NotImplementedError ``` Emit them by passing None. ``` test_object.empty_signal.emit(None) ``` --- src/sdbus/dbus_proxy_async_signal.py | 2 ++ test/test_sd_bus_async.py | 19 +++++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/src/sdbus/dbus_proxy_async_signal.py b/src/sdbus/dbus_proxy_async_signal.py index 7d834cc..be8d8f2 100644 --- a/src/sdbus/dbus_proxy_async_signal.py +++ b/src/sdbus/dbus_proxy_async_signal.py @@ -217,6 +217,8 @@ def _emit_message(self, args: T) -> None: isinstance(args, tuple)): signal_message.append_data( self.dbus_signal.signal_signature, *args) + elif self.dbus_signal.signal_signature == '' and args is None: + ... else: signal_message.append_data( self.dbus_signal.signal_signature, args) diff --git a/test/test_sd_bus_async.py b/test/test_sd_bus_async.py index 8cd5699..b2e87a0 100644 --- a/test/test_sd_bus_async.py +++ b/test/test_sd_bus_async.py @@ -198,6 +198,10 @@ async def test_struct_return_workaround(self) -> Tuple[Tuple[str, str]]: async def looong_method(self) -> None: await sleep(100) + @dbus_signal_async() + def empty_signal(self) -> None: + raise NotImplementedError + class DbusErrorTest(DbusFailedError): dbus_error_name = 'org.example.Error' @@ -802,3 +806,18 @@ async def test_properties_get_all_dict(self) -> None: await test_object_connection.properties_get_all_dict() )['test_property'], ) + + async def test_empty_signal(self) -> None: + test_object, test_object_connection = initialize_object() + + loop = get_running_loop() + + ai_dbus = test_object_connection.empty_signal.__aiter__() + aw_dbus = ai_dbus.__anext__() + q = test_object.empty_signal._get_local_queue() + + loop.call_at(0, test_object.empty_signal.emit, None) + + self.assertIsNone(await wait_for(aw_dbus, timeout=1)) + + self.assertIsNone(await wait_for(q.get(), timeout=1)) From 93fb0bd7a26945a898224f82c2b9901e94e2041d Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sun, 16 Oct 2022 22:05:23 +0300 Subject: [PATCH 003/188] Added Community interfaces section to README Thanks @bernhardkaindl for working on systemd binds --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index aa22e25..50b6597 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,10 @@ for tutorial and API reference. More incoming. (systemd, Bluez, screen saver... ) +### Community interfaces + +* [systemd](https://github.com/bernhardkaindl/python-sdbus-systemd) (by [@bernhardkaindl](https://github.com/bernhardkaindl)) + ## Requirements ### Binary package from PyPI From d24c33ac0577de21ead760f878f3dc42966fc465 Mon Sep 17 00:00:00 2001 From: igo95862 Date: Mon, 26 Dec 2022 14:32:49 +0300 Subject: [PATCH 004/188] Create CodeQL action Seems to be the replacement for lgtm --- .github/workflows/codeql.yml | 63 ++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 .github/workflows/codeql.yml diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..8a67e5c --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,63 @@ +--- +name: "CodeQL" + +on: + workflow_dispatch: + push: + branches: [ "master" ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ "master" ] + schedule: + - cron: '43 21 * * 3' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'python' ] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] + # Use only 'java' to analyze code written in Java, Kotlin or both + # Use only 'javascript' to analyze code written in JavaScript, TypeScript or both + # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v2 + + # ℹ️ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + + # If the Autobuild fails above, remove it and uncomment the following three lines. + # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. + + # - run: | + # echo "Run, Build Application using script" + # ./location_of_script_within_repo/buildscript.sh + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 + with: + category: "/language:${{matrix.language}}" From 60f00278e43bb7e60302de85f04b5d7b76de118e Mon Sep 17 00:00:00 2001 From: igo95862 Date: Mon, 26 Dec 2022 14:39:51 +0300 Subject: [PATCH 005/188] Fix typing error for `properties_changed` signal --- src/sdbus/dbus_proxy_async_interfaces.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sdbus/dbus_proxy_async_interfaces.py b/src/sdbus/dbus_proxy_async_interfaces.py index 1b1c809..c7d6f9f 100644 --- a/src/sdbus/dbus_proxy_async_interfaces.py +++ b/src/sdbus/dbus_proxy_async_interfaces.py @@ -76,7 +76,7 @@ def __init__(self) -> None: @dbus_signal_async('sa{sv}as') def properties_changed(self) -> DBUS_PROPERTIES_CHANGED_TYPING: - ... + raise NotImplementedError @dbus_method_async('s', 'a{sv}', method_name='GetAll') async def _properties_get_all( From 5a60920f7f397e8eaa34d256e210c7f09a2c8a9b Mon Sep 17 00:00:00 2001 From: igo95862 Date: Mon, 26 Dec 2022 14:43:24 +0300 Subject: [PATCH 006/188] Replace LGTM badge with CodeQL --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 50b6597..d1033af 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,4 @@ -[![Total alerts](https://img.shields.io/lgtm/alerts/g/igo95862/python-sdbus.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/igo95862/python-sdbus/alerts/) -[![Language grade: Python](https://img.shields.io/lgtm/grade/python/g/igo95862/python-sdbus.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/igo95862/python-sdbus/context:python) +[![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) # Modern Python library for D-Bus From 91b9f24d8b0fcf12fd8f913d44d5abc210550119 Mon Sep 17 00:00:00 2001 From: igo95862 Date: Mon, 26 Dec 2022 14:52:57 +0300 Subject: [PATCH 007/188] Use new github checkout actions --- .github/workflows/ubuntu_pypi_test.yml | 2 +- .github/workflows/ubuntu_test.yml | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ubuntu_pypi_test.yml b/.github/workflows/ubuntu_pypi_test.yml index 07c0724..811ed91 100644 --- a/.github/workflows/ubuntu_pypi_test.yml +++ b/.github/workflows/ubuntu_pypi_test.yml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-20.04 steps: - name: Checkout - uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f + uses: actions/checkout@755da8c3cf115ac066823e79a1e1788f8940201b - name: Install dependencies run: | sudo apt update diff --git a/.github/workflows/ubuntu_test.yml b/.github/workflows/ubuntu_test.yml index 3b496de..d9eb77a 100644 --- a/.github/workflows/ubuntu_test.yml +++ b/.github/workflows/ubuntu_test.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-20.04 steps: - name: Checkout - uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f + uses: actions/checkout@755da8c3cf115ac066823e79a1e1788f8940201b - name: Install dependencies run: | sudo apt update @@ -29,7 +29,7 @@ jobs: runs-on: ubuntu-20.04 steps: - name: Checkout - uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f + uses: actions/checkout@755da8c3cf115ac066823e79a1e1788f8940201b - name: Install dependencies run: | sudo apt update @@ -48,7 +48,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f + uses: actions/checkout@755da8c3cf115ac066823e79a1e1788f8940201b - name: Install dependencies run: | sudo apt update @@ -64,7 +64,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f + uses: actions/checkout@755da8c3cf115ac066823e79a1e1788f8940201b - name: Build Alpine container run: | podman build --tag alpine-ci -f ./test/containers/Containerfile-alpine . From 0f73afb831bbf06ac0147ea6faca50808f7debed Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sat, 14 Jan 2023 17:10:06 +0600 Subject: [PATCH 008/188] Added `sdbus.exceptions` module that holds all exceptions Importing exceptions from `sdbus` module has been deprecated and will be removed in version 1.0.0 This was done to make the imports cleaner by reducing number of imports from root module. Also added the request name exceptions but they are not actually raised by the functions. --- DEPRECATIONS.md | 8 +++ docs/api_index.rst | 80 +++++++++++++----------- docs/asyncio_quick.rst | 2 +- docs/common_api.rst | 2 + docs/exceptions.rst | 28 +++++++++ docs/sync_quick.rst | 4 +- src/sdbus/exceptions.py | 107 +++++++++++++++++++++++++++++++++ src/sdbus/sd_bus_internals.c | 37 ++++++++++-- src/sdbus/sd_bus_internals.h | 16 +++-- src/sdbus/sd_bus_internals.py | 18 +++++- test/leak_tests.py | 3 +- test/test_high_level_errors.py | 2 +- test/test_low_level_errors.py | 2 +- test/test_request_name.py | 80 ++++++++++++++++++++++++ test/test_sd_bus_async.py | 12 ++-- test/test_sd_bus_sync.py | 3 +- 16 files changed, 346 insertions(+), 58 deletions(-) create mode 100644 src/sdbus/exceptions.py create mode 100644 test/test_request_name.py diff --git a/DEPRECATIONS.md b/DEPRECATIONS.md index 1f7e239..06e9146 100644 --- a/DEPRECATIONS.md +++ b/DEPRECATIONS.md @@ -1,5 +1,13 @@ # Deprecation information +## Importing exceptions from `sdbus` module + +All exceptions have been moved to `sdbus.exceptions` to clean up imports. + +* **Since**: 0.11.0 +* **Warning**: Not possible? +* **Removed**: 1.0.0 + ## `_connect` and `new_connect` of the `DbusInterfaceCommonAsync` class Replaced with equivalent `_proxify` and `new_proxy`. diff --git a/docs/api_index.rst b/docs/api_index.rst index df92255..bbb4c17 100644 --- a/docs/api_index.rst +++ b/docs/api_index.rst @@ -65,70 +65,82 @@ Blocking: Exceptions: ++++++++++++++++++++++++++ -:py:exc:`DbusAccessDeniedError` +:py:exc:`exceptions.DbusAccessDeniedError` -:py:exc:`DbusAddressInUseError` +:py:exc:`exceptions.DbusAccessDeniedError` -:py:exc:`DbusAuthFailedError` +:py:exc:`exceptions.DbusAddressInUseError` -:py:exc:`DbusBadAddressError` +:py:exc:`exceptions.DbusAuthFailedError` -:py:exc:`DbusDisconnectedError` +:py:exc:`exceptions.DbusBadAddressError` -:py:exc:`DbusFailedError` +:py:exc:`exceptions.DbusDisconnectedError` -:py:exc:`DbusFileExistsError` +:py:exc:`exceptions.DbusFailedError` -:py:exc:`DbusFileNotFoundError` +:py:exc:`exceptions.DbusFileExistsError` -:py:exc:`DbusInconsistentMessageError` +:py:exc:`exceptions.DbusFileNotFoundError` -:py:exc:`DbusInteractiveAuthorizationRequiredError` +:py:exc:`exceptions.DbusInconsistentMessageError` -:py:exc:`DbusInvalidArgsError` +:py:exc:`exceptions.DbusInteractiveAuthorizationRequiredError` -:py:exc:`DbusInvalidFileContentError` +:py:exc:`exceptions.DbusInvalidArgsError` -:py:exc:`DbusInvalidSignatureError` +:py:exc:`exceptions.DbusInvalidFileContentError` -:py:exc:`DbusIOError` +:py:exc:`exceptions.DbusInvalidSignatureError` -:py:exc:`DbusLimitsExceededError` +:py:exc:`exceptions.DbusIOError` -:py:exc:`DbusMatchRuleInvalidError` +:py:exc:`exceptions.DbusLimitsExceededError` -:py:exc:`DbusMatchRuleNotFound` +:py:exc:`exceptions.DbusMatchRuleInvalidError` -:py:exc:`DbusNameHasNoOwnerError` +:py:exc:`exceptions.DbusMatchRuleNotFound` -:py:exc:`DbusNoMemoryError` +:py:exc:`exceptions.DbusNameHasNoOwnerError` -:py:exc:`DbusNoNetworkError` +:py:exc:`exceptions.DbusNoMemoryError` -:py:exc:`DbusNoReplyError` +:py:exc:`exceptions.DbusNoNetworkError` -:py:exc:`DbusNoServerError` +:py:exc:`exceptions.DbusNoReplyError` -:py:exc:`DbusNotSupportedError` +:py:exc:`exceptions.DbusNoServerError` -:py:exc:`DbusPropertyReadOnlyError` +:py:exc:`exceptions.DbusNotSupportedError` -:py:exc:`DbusServiceUnknownError` +:py:exc:`exceptions.DbusPropertyReadOnlyError` -:py:exc:`DbusTimeoutError` +:py:exc:`exceptions.DbusServiceUnknownError` -:py:exc:`DbusUnixProcessIdUnknownError` +:py:exc:`exceptions.DbusTimeoutError` -:py:exc:`DbusUnknownInterfaceError` +:py:exc:`exceptions.DbusUnixProcessIdUnknownError` -:py:exc:`DbusUnknownMethodError` +:py:exc:`exceptions.DbusUnknownInterfaceError` -:py:exc:`DbusUnknownObjectError` +:py:exc:`exceptions.DbusUnknownMethodError` -:py:exc:`DbusUnknownPropertyError` +:py:exc:`exceptions.DbusUnknownObjectError` -:py:exc:`SdBusBaseError` +:py:exc:`exceptions.DbusUnknownPropertyError` -:py:exc:`SdBusLibraryError` +:py:exc:`exceptions.SdBusBaseError` -:py:exc:`SdBusUnmappedMessageError` \ No newline at end of file +:py:exc:`exceptions.SdBusLibraryError` + +:py:exc:`exceptions.SdBusUnmappedMessageError` + +:py:func:`exceptions.map_exception_to_dbus_error` + +:py:exc:`exceptions.SdBusRequestNameError` + +:py:exc:`exceptions.SdBusRequestNameInQueueError` + +:py:exc:`exceptions.SdBusRequestNameExistsError` + +:py:exc:`exceptions.SdBusRequestNameAlreadyOwnerError` diff --git a/docs/asyncio_quick.rst b/docs/asyncio_quick.rst index dac3c01..61b72e7 100644 --- a/docs/asyncio_quick.rst +++ b/docs/asyncio_quick.rst @@ -135,7 +135,7 @@ Methods have to be async function, otherwise :py:exc:`AssertionError` will be ra While method calls are async there is a inherit timeout timer for any method call. -To return an error to caller you need to raise exception which has a :py:exc:`DbusFailedError` as base. +To return an error to caller you need to raise exception which has a :py:exc:`.DbusFailedError` as base. Regular exceptions will not propagate. See :doc:`/exceptions`. diff --git a/docs/common_api.rst b/docs/common_api.rst index e61eea4..7f231d4 100644 --- a/docs/common_api.rst +++ b/docs/common_api.rst @@ -15,6 +15,7 @@ Dbus connections calls :param str new_name: the name to acquire. Must be a valid dbus service name. + :raises: :ref:`name-request-exceptions` and other D-Bus exceptions. .. py:function:: request_default_bus_name(new_name) @@ -22,6 +23,7 @@ Dbus connections calls :param str new_name: the name to acquire. Must be a valid dbus service name. + :raises: :ref:`name-request-exceptions` and other D-Bus exceptions. .. py:function:: set_default_bus(new_default) diff --git a/docs/exceptions.rst b/docs/exceptions.rst index d9e4cdf..2be6f24 100644 --- a/docs/exceptions.rst +++ b/docs/exceptions.rst @@ -1,6 +1,8 @@ Exceptions ======================== +.. py:currentmodule:: sdbus.exceptions + Error name bound exceptions +++++++++++++++++++++++++++++++ @@ -80,6 +82,32 @@ Other exceptions Exception message contains line number and the error name. +.. _name-request-exceptions: + +Name request exceptions ++++++++++++++++++++++++ + +These exceptions will be raise if an error related to ownership of D-Bus +names occurs when calling :py:func:`.request_default_bus_name_async` or +:py:func:`.request_default_bus_name`. + +.. py:exception:: SdBusRequestNameError + + Common base exception for any name ownership error. + +.. py:exception:: SdBusRequestNameInQueueError + + Someone already owns the name but the request has been placed in queue. + +.. py:exception:: SdBusRequestNameExistsError + + Someone already owns the name. + +.. py:exception:: SdBusRequestNameAlreadyOwnerError + + The caller already owns the name. + + .. _list of error exceptions: Error name exception list diff --git a/docs/sync_quick.rst b/docs/sync_quick.rst index 6c9e695..f28eac9 100644 --- a/docs/sync_quick.rst +++ b/docs/sync_quick.rst @@ -76,7 +76,7 @@ Methods Methods are functions wrapped with :py:func:`dbus_method` decorator. -If the remote object sends an error reply an exception with base of :py:exc:`DbusFailedError` +If the remote object sends an error reply an exception with base of :py:exc:`.DbusFailedError` will be raised. See :doc:`/exceptions` for list of exceptions. The wrapped function will not be called. Its recommended to set the function to ``raise NotImplementedError``. @@ -128,7 +128,7 @@ The new property behaves very similar to Pythons :py:func:`property` decorator. # Assign new string d.test_string = 'some_string' -If property is read-only when :py:exc:`DbusPropertyReadOnlyError` will be raised. +If property is read-only when :py:exc:`.DbusPropertyReadOnlyError` will be raised. Multiple interfaces ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/src/sdbus/exceptions.py b/src/sdbus/exceptions.py new file mode 100644 index 0000000..7dfb403 --- /dev/null +++ b/src/sdbus/exceptions.py @@ -0,0 +1,107 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later + +# Copyright (C) 2023 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 .dbus_exceptions import ( + DbusAccessDeniedError, + DbusAddressInUseError, + DbusAuthFailedError, + DbusBadAddressError, + DbusDisconnectedError, + DbusFailedError, + DbusFileExistsError, + DbusFileNotFoundError, + DbusInconsistentMessageError, + DbusInteractiveAuthorizationRequiredError, + DbusInvalidArgsError, + DbusInvalidFileContentError, + DbusInvalidSignatureError, + DbusIOError, + DbusLimitsExceededError, + DbusMatchRuleInvalidError, + DbusMatchRuleNotFound, + DbusNameHasNoOwnerError, + DbusNoMemoryError, + DbusNoNetworkError, + DbusNoReplyError, + DbusNoServerError, + DbusNotSupportedError, + DbusPropertyReadOnlyError, + DbusServiceUnknownError, + DbusTimeoutError, + DbusUnixProcessIdUnknownError, + DbusUnknownInterfaceError, + DbusUnknownMethodError, + DbusUnknownObjectError, + DbusUnknownPropertyError, +) +from .sd_bus_internals import ( + SdBusBaseError, + SdBusLibraryError, + SdBusRequestNameAlreadyOwnerError, + SdBusRequestNameError, + SdBusRequestNameExistsError, + SdBusRequestNameInQueueError, + SdBusUnmappedMessageError, + map_exception_to_dbus_error, +) + +__all__ = ( + 'DbusAccessDeniedError', + 'DbusAddressInUseError', + 'DbusAuthFailedError', + 'DbusBadAddressError', + 'DbusDisconnectedError', + 'DbusFailedError', + 'DbusFileExistsError', + 'DbusFileNotFoundError', + 'DbusInconsistentMessageError', + 'DbusInteractiveAuthorizationRequiredError', + 'DbusInvalidArgsError', + 'DbusInvalidFileContentError', + 'DbusInvalidSignatureError', + 'DbusIOError', + 'DbusLimitsExceededError', + 'DbusMatchRuleInvalidError', + 'DbusMatchRuleNotFound', + 'DbusNameHasNoOwnerError', + 'DbusNoMemoryError', + 'DbusNoNetworkError', + 'DbusNoReplyError', + 'DbusNoServerError', + 'DbusNotSupportedError', + 'DbusPropertyReadOnlyError', + 'DbusServiceUnknownError', + 'DbusTimeoutError', + 'DbusUnixProcessIdUnknownError', + 'DbusUnknownInterfaceError', + 'DbusUnknownMethodError', + 'DbusUnknownObjectError', + 'DbusUnknownPropertyError', + 'map_exception_to_dbus_error', + + 'SdBusBaseError', + 'SdBusLibraryError', + 'SdBusRequestNameAlreadyOwnerError', + 'SdBusRequestNameError', + 'SdBusRequestNameExistsError', + 'SdBusRequestNameInQueueError', + 'SdBusUnmappedMessageError', +) diff --git a/src/sdbus/sd_bus_internals.c b/src/sdbus/sd_bus_internals.c index c7c887a..a2b2345 100644 --- a/src/sdbus/sd_bus_internals.c +++ b/src/sdbus/sd_bus_internals.c @@ -21,11 +21,6 @@ #include "sd_bus_internals.h" // Python functions and objects -PyObject* unmapped_error_exception = NULL; -PyObject* dbus_error_to_exception_dict = NULL; -PyObject* exception_to_dbus_error_dict = NULL; -PyObject* exception_base = NULL; -PyObject* exception_lib = NULL; PyObject* asyncio_get_running_loop = NULL; PyObject* asyncio_queue_class = NULL; PyObject* is_coroutine_function = NULL; @@ -41,6 +36,17 @@ PyObject* extend_str = NULL; PyObject* append_str = NULL; PyObject* call_soon_str = NULL; PyObject* create_task_str = NULL; +// Exceptions +PyObject* exception_base = NULL; +PyObject* unmapped_error_exception = NULL; +PyObject* exception_lib = NULL; +PyObject* exception_request_name = NULL; // Base to any request name exception +PyObject* exception_request_name_in_queue = NULL; // Queued up to acquire name +PyObject* exception_request_name_exists = NULL; // Someone already owns the name +PyObject* exception_request_name_already_owner = NULL; // Already an owner of the name + +PyObject* dbus_error_to_exception_dict = NULL; +PyObject* exception_to_dbus_error_dict = NULL; // SdBusSlot @@ -123,6 +129,27 @@ PyMODINIT_FUNC PyInit_sd_bus_internals(void) { SD_BUS_PY_INIT_ADD_OBJECT("SdBusLibraryError", library_exception); exception_lib = library_exception; + // Request name exceptions + PyObject* request_name_exception CLEANUP_PY_OBJECT = + CALL_PYTHON_AND_CHECK(PyErr_NewException("sd_bus_internals.SdBusRequestNameError", new_base_exception, NULL)); + SD_BUS_PY_INIT_ADD_OBJECT("SdBusRequestNameError", request_name_exception); + exception_request_name = request_name_exception; + // Request name but in queue + PyObject* request_name_in_queue_exception CLEANUP_PY_OBJECT = + CALL_PYTHON_AND_CHECK(PyErr_NewException("sd_bus_internals.SdBusRequestNameInQueueError", request_name_exception, NULL)); + SD_BUS_PY_INIT_ADD_OBJECT("SdBusRequestNameInQueueError", request_name_in_queue_exception); + exception_request_name_in_queue = request_name_in_queue_exception; + // Request name but someone already owns the name + PyObject* request_name_exists_exception CLEANUP_PY_OBJECT = + CALL_PYTHON_AND_CHECK(PyErr_NewException("sd_bus_internals.SdBusRequestNameExistsError", request_name_exception, NULL)); + SD_BUS_PY_INIT_ADD_OBJECT("SdBusRequestNameExistsError", request_name_exists_exception); + exception_request_name_exists = request_name_exists_exception; + // Request name but we already own the name + PyObject* request_name_already_owner_exception CLEANUP_PY_OBJECT = + CALL_PYTHON_AND_CHECK(PyErr_NewException("sd_bus_internals.SdBusRequestNameAlreadyOwnerError", request_name_exception, NULL)); + SD_BUS_PY_INIT_ADD_OBJECT("SdBusRequestNameAlreadyOwnerError", request_name_already_owner_exception); + exception_request_name_already_owner = request_name_already_owner_exception; + PyObject* asyncio_module = CALL_PYTHON_AND_CHECK(PyImport_ImportModule("asyncio")); asyncio_get_running_loop = CALL_PYTHON_AND_CHECK(PyObject_GetAttrString(asyncio_module, "get_running_loop")); diff --git a/src/sdbus/sd_bus_internals.h b/src/sdbus/sd_bus_internals.h index a928e2b..7fbe24b 100644 --- a/src/sdbus/sd_bus_internals.h +++ b/src/sdbus/sd_bus_internals.h @@ -239,11 +239,6 @@ #endif // Python functions and objects -extern PyObject* unmapped_error_exception; -extern PyObject* dbus_error_to_exception_dict; -extern PyObject* exception_to_dbus_error_dict; -extern PyObject* exception_base; -extern PyObject* exception_lib; extern PyObject* asyncio_get_running_loop; extern PyObject* asyncio_queue_class; extern PyObject* is_coroutine_function; @@ -259,6 +254,17 @@ extern PyObject* extend_str; extern PyObject* append_str; extern PyObject* call_soon_str; extern PyObject* create_task_str; +// Exceptions +extern PyObject* exception_base; +extern PyObject* unmapped_error_exception; +extern PyObject* exception_lib; +extern PyObject* exception_request_name; // Base to any request name exception +extern PyObject* exception_request_name_in_queue; // Queued up to acquire name +extern PyObject* exception_request_name_exists; // Someone already owns the name +extern PyObject* exception_request_name_already_owner; // Already an owner of the name + +extern PyObject* dbus_error_to_exception_dict; +extern PyObject* exception_to_dbus_error_dict; __attribute__((used)) static inline void _cleanup_char_ptr(const char** ptr) { if (*ptr != NULL) { diff --git a/src/sdbus/sd_bus_internals.py b/src/sdbus/sd_bus_internals.py index c3d9a26..c05f480 100644 --- a/src/sdbus/sd_bus_internals.py +++ b/src/sdbus/sd_bus_internals.py @@ -1,6 +1,6 @@ # SPDX-License-Identifier: LGPL-2.1-or-later -# Copyright (C) 2020, 2021 igo95862 +# Copyright (C) 2020-2023 igo95862 # This file is part of python-sdbus @@ -288,6 +288,22 @@ class SdBusLibraryError(SdBusBaseError): ... +class SdBusRequestNameError(SdBusBaseError): + ... + + +class SdBusRequestNameInQueueError(SdBusRequestNameError): + ... + + +class SdBusRequestNameExistsError(SdBusRequestNameError): + ... + + +class SdBusRequestNameAlreadyOwnerError(SdBusRequestNameError): + ... + + DBUS_ERROR_TO_EXCEPTION: Dict[str, Exception] = {} EXCEPTION_TO_DBUS_ERROR: Dict[Exception, str] = {} diff --git a/test/leak_tests.py b/test/leak_tests.py index c9a7037..03604e8 100644 --- a/test/leak_tests.py +++ b/test/leak_tests.py @@ -32,9 +32,10 @@ from typing import Any, List, cast from unittest import SkipTest +from sdbus.exceptions import DbusFailedError from sdbus.unittest import IsolatedDbusTestCase -from sdbus import DbusFailedError, request_default_bus_name_async +from sdbus import request_default_bus_name_async from .test_low_level_errors import ( DbusDerivePropertydError, diff --git a/test/test_high_level_errors.py b/test/test_high_level_errors.py index 02df8b0..3bfb873 100644 --- a/test/test_high_level_errors.py +++ b/test/test_high_level_errors.py @@ -22,10 +22,10 @@ from asyncio import get_running_loop, wait_for from typing import Any +from sdbus.exceptions import DbusFailedError from sdbus.unittest import IsolatedDbusTestCase from sdbus import ( - DbusFailedError, DbusInterfaceCommonAsync, dbus_method_async, request_default_bus_name_async, diff --git a/test/test_low_level_errors.py b/test/test_low_level_errors.py index 6f6e48c..b264e74 100644 --- a/test/test_low_level_errors.py +++ b/test/test_low_level_errors.py @@ -22,10 +22,10 @@ from asyncio import get_running_loop, wait_for from typing import Any +from sdbus.exceptions import DbusFailedError from sdbus.unittest import IsolatedDbusTestCase from sdbus import ( - DbusFailedError, DbusInterfaceCommonAsync, dbus_method_async, dbus_property_async, diff --git a/test/test_request_name.py b/test/test_request_name.py new file mode 100644 index 0000000..8a916fc --- /dev/null +++ b/test/test_request_name.py @@ -0,0 +1,80 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later + +# Copyright (C) 2023 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 unittest import main + +from sdbus.exceptions import ( + SdBusRequestNameAlreadyOwnerError, + SdBusRequestNameError, + SdBusRequestNameExistsError, + SdBusRequestNameInQueueError, +) +from sdbus.unittest import IsolatedDbusTestCase + + +class TestRequestName(IsolatedDbusTestCase): + async def asyncSetUp(self) -> None: + await super().asyncSetUp() + + def test_request_name_exception_tree(self) -> None: + # Test that SdBusRequestNameError is super class + # of other request name exceptions + self.assertTrue( + issubclass( + SdBusRequestNameAlreadyOwnerError, + SdBusRequestNameError, + ) + ) + self.assertTrue( + issubclass( + SdBusRequestNameExistsError, + SdBusRequestNameError, + ) + ) + self.assertTrue( + issubclass( + SdBusRequestNameInQueueError, + SdBusRequestNameError, + ) + ) + # Test the opposite + self.assertFalse( + issubclass( + SdBusRequestNameAlreadyOwnerError, + SdBusRequestNameExistsError, + ) + ) + self.assertFalse( + issubclass( + SdBusRequestNameInQueueError, + SdBusRequestNameExistsError, + ) + ) + self.assertFalse( + issubclass( + SdBusRequestNameInQueueError, + SdBusRequestNameAlreadyOwnerError, + ) + ) + + +if __name__ == '__main__': + main() diff --git a/test/test_sd_bus_async.py b/test/test_sd_bus_async.py index b2e87a0..87aa0c2 100644 --- a/test/test_sd_bus_async.py +++ b/test/test_sd_bus_async.py @@ -26,6 +26,13 @@ from unittest import SkipTest from sdbus.dbus_common_funcs import PROPERTY_FLAGS_MASK, count_bits +from sdbus.exceptions import ( + DbusFailedError, + DbusFileExistsError, + DbusUnknownObjectError, + SdBusLibraryError, + SdBusUnmappedMessageError, +) from sdbus.sd_bus_internals import ( DBUS_ERROR_TO_EXCEPTION, DbusDeprecatedFlag, @@ -36,13 +43,8 @@ from sdbus.unittest import IsolatedDbusTestCase from sdbus import ( - DbusFailedError, - DbusFileExistsError, DbusInterfaceCommonAsync, DbusNoReplyFlag, - DbusUnknownObjectError, - SdBusLibraryError, - SdBusUnmappedMessageError, dbus_method_async, dbus_method_async_override, dbus_property_async, diff --git a/test/test_sd_bus_sync.py b/test/test_sd_bus_sync.py index 724e80e..1c25e50 100644 --- a/test/test_sd_bus_sync.py +++ b/test/test_sd_bus_sync.py @@ -22,11 +22,10 @@ from unittest import main +from sdbus.exceptions import DbusPropertyReadOnlyError from sdbus.unittest import IsolatedDbusTestCase from sdbus_block.dbus_daemon import FreedesktopDbus -from sdbus import DbusPropertyReadOnlyError - class TestSync(IsolatedDbusTestCase): From 6a7264f95544ef3b5c38b199bdec57d66bd52a3f Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sat, 14 Jan 2023 17:59:37 +0600 Subject: [PATCH 009/188] Added internal name requests flags Can be used to specify behavior of name requests. --- src/sdbus/sd_bus_internals.c | 4 ++++ src/sdbus/sd_bus_internals.py | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/src/sdbus/sd_bus_internals.c b/src/sdbus/sd_bus_internals.c index a2b2345..670a782 100644 --- a/src/sdbus/sd_bus_internals.c +++ b/src/sdbus/sd_bus_internals.c @@ -181,6 +181,10 @@ PyMODINIT_FUNC PyInit_sd_bus_internals(void) { CALL_PYTHON_INT_CHECK(PyModule_AddIntConstant(m, "DbusPropertyExplicitFlag", SD_BUS_VTABLE_PROPERTY_EXPLICIT)); CALL_PYTHON_INT_CHECK(PyModule_AddIntConstant(m, "DbusSensitiveFlag", SD_BUS_VTABLE_SENSITIVE)); + CALL_PYTHON_INT_CHECK(PyModule_AddIntConstant(m, "NameAllowReplacementFlag", SD_BUS_NAME_ALLOW_REPLACEMENT)); + CALL_PYTHON_INT_CHECK(PyModule_AddIntConstant(m, "NameReplaceExistingFlag", SD_BUS_NAME_REPLACE_EXISTING)); + CALL_PYTHON_INT_CHECK(PyModule_AddIntConstant(m, "NameQueueFlag", SD_BUS_NAME_QUEUE)); + Py_INCREF(m); return m; } diff --git a/src/sdbus/sd_bus_internals.py b/src/sdbus/sd_bus_internals.py index c05f480..cdc5924 100644 --- a/src/sdbus/sd_bus_internals.py +++ b/src/sdbus/sd_bus_internals.py @@ -317,3 +317,7 @@ class SdBusRequestNameAlreadyOwnerError(SdBusRequestNameError): DbusPropertyEmitsInvalidationFlag: int = 0 DbusPropertyExplicitFlag: int = 0 DbusSensitiveFlag: int = 0 + +NameAllowReplacementFlag: int = 0 +NameReplaceExistingFlag: int = 0 +NameQueueFlag: int = 0 From a8bcbd217d7d92d8ab8872ec824f714d4715efd4 Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sat, 14 Jan 2023 19:52:54 +0600 Subject: [PATCH 010/188] Fix D-Bus name requests not raising appropriate exceptions `SdBusRequestNameExistsError`: Someone already owns name `SdBusRequestNameAlreadyOwnerError`: Caller already owns name `SdBusRequestNameInQueueError`: Name request queued up --- src/sdbus/sd_bus_internals_bus.c | 61 +++++++++++++++++++----- test/test_request_name.py | 82 ++++++++++++++++++++++++++++++++ 2 files changed, 130 insertions(+), 13 deletions(-) diff --git a/src/sdbus/sd_bus_internals_bus.c b/src/sdbus/sd_bus_internals_bus.c index fb920b4..8597858 100644 --- a/src/sdbus/sd_bus_internals_bus.c +++ b/src/sdbus/sd_bus_internals_bus.c @@ -468,9 +468,9 @@ static PyObject* SdBus_get_signal_queue(SdBusObject* self, PyObject* args) { return new_future; } -int SdBus_request_callback(sd_bus_message* m, - void* userdata, // Should be the asyncio.Future - sd_bus_error* Py_UNUSED(ret_error)) { +int SdBus_request_name_callback(sd_bus_message* m, + void* userdata, // Should be the asyncio.Future + sd_bus_error* Py_UNUSED(ret_error)) { PyObject* py_future = userdata; PyObject* is_cancelled CLEANUP_PY_OBJECT = PyObject_CallMethod(py_future, "cancelled", ""); if (Py_True == is_cancelled) { @@ -479,11 +479,31 @@ int SdBus_request_callback(sd_bus_message* m, } if (!sd_bus_message_is_method_error(m, NULL)) { - // Not Error, set Future result to new message object - PyObject* return_object CLEANUP_PY_OBJECT = PyObject_CallMethod(py_future, "set_result", "O", Py_None); - if (return_object == NULL) { - return -1; + uint32_t request_name_result = 0; + CALL_SD_BUS_CHECK_RETURN_NEG1(sd_bus_message_read_basic(m, 'u', &request_name_result)); + if (1 == request_name_result) { + // Successfully acquired the name + Py_XDECREF(CALL_PYTHON_CHECK_RETURN_NEG1(PyObject_CallMethod(py_future, "set_result", "O", Py_None))); + return 0; } + + PyObject* exception_to_raise CLEANUP_PY_OBJECT = NULL; + switch (request_name_result) { + case 2: + exception_to_raise = CALL_PYTHON_CHECK_RETURN_NEG1(PyObject_CallFunctionObjArgs(exception_request_name_in_queue, NULL)); + break; + case 3: + exception_to_raise = CALL_PYTHON_CHECK_RETURN_NEG1(PyObject_CallFunctionObjArgs(exception_request_name_exists, NULL)); + break; + case 4: + exception_to_raise = CALL_PYTHON_CHECK_RETURN_NEG1(PyObject_CallFunctionObjArgs(exception_request_name_already_owner, NULL)); + break; + default: + exception_to_raise = CALL_PYTHON_CHECK_RETURN_NEG1(PyObject_CallFunctionObjArgs(exception_request_name, NULL)); + break; + } + Py_XDECREF(CALL_PYTHON_CHECK_RETURN_NEG1(PyObject_CallMethod(py_future, "set_exception", "O", exception_to_raise))); + return -1; } else { // An Error, set exception if (future_set_exception_from_message(py_future, m) < 0) { @@ -517,11 +537,9 @@ static PyObject* SdBus_request_name_async(SdBusObject* self, PyObject* args) { SdBusSlotObject* new_slot_object CLEANUP_SD_BUS_SLOT = (SdBusSlotObject*)CALL_PYTHON_AND_CHECK(SD_BUS_PY_CLASS_DUNDER_NEW(SdBusSlot_class)); CALL_SD_BUS_AND_CHECK( - sd_bus_request_name_async(self->sd_bus_ref, &new_slot_object->slot_ref, service_name_char_ptr, flags, SdBus_request_callback, new_future)); + sd_bus_request_name_async(self->sd_bus_ref, &new_slot_object->slot_ref, service_name_char_ptr, flags, SdBus_request_name_callback, new_future)); - if (PyObject_SetAttrString(new_future, "_sd_bus_py_slot", (PyObject*)new_slot_object) < 0) { - return NULL; - } + CALL_PYTHON_INT_CHECK(PyObject_SetAttrString(new_future, "_sd_bus_py_slot", (PyObject*)new_slot_object)); CHECK_SD_BUS_READER; return new_future; } @@ -544,8 +562,25 @@ static PyObject* SdBus_request_name(SdBusObject* self, PyObject* args) { CALL_PYTHON_BOOL_CHECK(PyArg_ParseTuple(args, "sK", &service_name_char_ptr, &flags_long_long, NULL)); uint64_t flags = (uint64_t)flags_long_long; #endif - CALL_SD_BUS_AND_CHECK(sd_bus_request_name(self->sd_bus_ref, service_name_char_ptr, flags)); - Py_RETURN_NONE; + int request_name_return_code = sd_bus_request_name(self->sd_bus_ref, service_name_char_ptr, flags); + switch (request_name_return_code) { + case -EEXIST: + return PyErr_Format(exception_request_name_exists, "Name \"%s\" already owned.", service_name_char_ptr, NULL); + break; + case -EALREADY: + return PyErr_Format(exception_request_name_already_owner, "Already own name \"%s\".", service_name_char_ptr, NULL); + break; + case 0: + return PyErr_Format(exception_request_name_in_queue, "Queued up to acquire name \"%s\".", service_name_char_ptr, NULL); + break; + case 1: + Py_RETURN_NONE; + break; + default: + CALL_SD_BUS_AND_CHECK(request_name_return_code); + break; + } + Py_UNREACHABLE(); } #ifndef Py_LIMITED_API diff --git a/test/test_request_name.py b/test/test_request_name.py index 8a916fc..3855fa0 100644 --- a/test/test_request_name.py +++ b/test/test_request_name.py @@ -19,16 +19,24 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA from __future__ import annotations +from asyncio import wait_for from unittest import main from sdbus.exceptions import ( + SdBusLibraryError, SdBusRequestNameAlreadyOwnerError, SdBusRequestNameError, SdBusRequestNameExistsError, SdBusRequestNameInQueueError, ) +from sdbus.sd_bus_internals import NameQueueFlag from sdbus.unittest import IsolatedDbusTestCase +from sdbus import sd_bus_open_user + +TEST_BUS_NAME = 'com.example.test' +TEST_BUS_NAME_regex_match = TEST_BUS_NAME.replace('.', r'\.') + class TestRequestName(IsolatedDbusTestCase): async def asyncSetUp(self) -> None: @@ -75,6 +83,80 @@ def test_request_name_exception_tree(self) -> None: ) ) + async def test_name_exists_async(self) -> None: + extra_bus = sd_bus_open_user() + await self.bus.request_name_async(TEST_BUS_NAME, 0) + + with self.assertRaises(SdBusRequestNameExistsError): + await wait_for( + extra_bus.request_name_async(TEST_BUS_NAME, 0), + timeout=1, + ) + + async def test_name_already_async(self) -> None: + await self.bus.request_name_async(TEST_BUS_NAME, 0) + + with self.assertRaises(SdBusRequestNameAlreadyOwnerError): + await wait_for( + self.bus.request_name_async(TEST_BUS_NAME, 0), + timeout=1, + ) + + async def test_name_queued_async(self) -> None: + extra_bus = sd_bus_open_user() + await self.bus.request_name_async(TEST_BUS_NAME, 0) + + with self.assertRaises(SdBusRequestNameInQueueError): + await wait_for( + extra_bus.request_name_async(TEST_BUS_NAME, NameQueueFlag), + timeout=1, + ) + + async def test_name_other_error_async(self) -> None: + extra_bus = sd_bus_open_user() + extra_bus.close() + + with self.assertRaises(SdBusLibraryError): + await wait_for( + extra_bus.request_name_async(TEST_BUS_NAME, 0), + timeout=1, + ) + + def test_name_exists_block(self) -> None: + extra_bus = sd_bus_open_user() + self.bus.request_name(TEST_BUS_NAME, 0) + + with self.assertRaisesRegex( + SdBusRequestNameExistsError, + TEST_BUS_NAME_regex_match, + ): + extra_bus.request_name(TEST_BUS_NAME, 0) + + def test_name_already_block(self) -> None: + self.bus.request_name(TEST_BUS_NAME, 0) + + with self.assertRaisesRegex( + SdBusRequestNameAlreadyOwnerError, + TEST_BUS_NAME_regex_match, + ): + self.bus.request_name(TEST_BUS_NAME, 0) + + def test_name_queued_block(self) -> None: + extra_bus = sd_bus_open_user() + self.bus.request_name(TEST_BUS_NAME, 0) + + with self.assertRaisesRegex( + SdBusRequestNameInQueueError, + TEST_BUS_NAME_regex_match, + ): + extra_bus.request_name(TEST_BUS_NAME, NameQueueFlag) + + def test_name_other_error_block(self) -> None: + extra_bus = sd_bus_open_user() + extra_bus.close() + with self.assertRaises(SdBusLibraryError): + extra_bus.request_name(TEST_BUS_NAME, 0) + if __name__ == '__main__': main() From b246d56e5c1e3f2fa76087b26162f1b731306751 Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sat, 14 Jan 2023 21:12:28 +0600 Subject: [PATCH 011/188] Added boolean arguments to the default bus name request functions The undocumented argument `flags` was removed. Since it was not documented it is removed without going through deprecation. The boolean flags provide the control over the name acquisition behavior. Looks like the `request_default_bus_name` was async on accident. Add a special deprecation warning on awaiting the return object. --- DEPRECATIONS.md | 10 +++++ docs/common_api.rst | 22 +++++++++- src/sdbus/dbus_common_funcs.py | 73 +++++++++++++++++++++++++++++----- test/test_deprecations.py | 39 ++++++++++++++++++ test/test_request_name.py | 59 ++++++++++++++++++++++++--- 5 files changed, 185 insertions(+), 18 deletions(-) create mode 100644 test/test_deprecations.py diff --git a/DEPRECATIONS.md b/DEPRECATIONS.md index 06e9146..08ecdfd 100644 --- a/DEPRECATIONS.md +++ b/DEPRECATIONS.md @@ -1,5 +1,15 @@ # Deprecation information +## Awaiting on `request_default_bus_name` + +By mistake `request_default_bus_name` was made in to async function +even though it was never documented to be one. It is now a blocking +function but returns an awaitable for backwards compatibility. + +* **Since**: 0.11.0 +* **Warning**: 0.11.0 +* **Removed**: 1.0.0 + ## Importing exceptions from `sdbus` module All exceptions have been moved to `sdbus.exceptions` to clean up imports. diff --git a/docs/common_api.rst b/docs/common_api.rst index 7f231d4..b19d183 100644 --- a/docs/common_api.rst +++ b/docs/common_api.rst @@ -8,21 +8,39 @@ These calls are shared between async and blocking API. Dbus connections calls ++++++++++++++++++++++++++++++++++ -.. py:function:: request_default_bus_name_async(new_name) +.. py:function:: request_default_bus_name_async(new_name, allow_replacement, replace_existing, queue) :async: Acquire a name on the default bus async. :param str new_name: the name to acquire. Must be a valid dbus service name. + :param str new_name: the name to acquire. + Must be a valid dbus service name. + :param bool allow_replacement: If name was acquired allow other peers + to take away the name. + :param bool replace_existing: If current name owner allows, take + away the name. + :param bool queue: Queue up for name acquisition. + :py:exc:`.SdBusRequestNameInQueueError` will be raised when successfully + placed in queue. :py:meth:`Ownership change signal ` + should be monitored get notified when the name was acquired. :raises: :ref:`name-request-exceptions` and other D-Bus exceptions. -.. py:function:: request_default_bus_name(new_name) +.. py:function:: request_default_bus_name(new_name, allow_replacement, replace_existing, queue) Acquire a name on the default bus. :param str new_name: the name to acquire. Must be a valid dbus service name. + :param bool allow_replacement: If name was acquired allow other peers + to take away the name. + :param bool replace_existing: If current name owner allows, take + away the name. + :param bool queue: Queue up for name acquisition. + :py:exc:`.SdBusRequestNameInQueueError` will be raised when successfully + placed in queue. :py:meth:`Ownership change signal ` + should be monitored get notified when the name was acquired. :raises: :ref:`name-request-exceptions` and other D-Bus exceptions. .. py:function:: set_default_bus(new_default) diff --git a/src/sdbus/dbus_common_funcs.py b/src/sdbus/dbus_common_funcs.py index 486cad7..df3293d 100644 --- a/src/sdbus/dbus_common_funcs.py +++ b/src/sdbus/dbus_common_funcs.py @@ -1,7 +1,7 @@ # SPDX-License-Identifier: LGPL-2.1-or-later -# Copyright (C) 2020-2022 igo95862 +# Copyright (C) 2020-2023 igo95862 # This file is part of python-sdbus @@ -20,15 +20,19 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA from __future__ import annotations -from asyncio import get_running_loop +from asyncio import Future, get_running_loop from contextvars import ContextVar -from typing import Iterator +from typing import Generator, Iterator +from warnings import warn from .sd_bus_internals import ( DbusPropertyConstFlag, DbusPropertyEmitsChangeFlag, DbusPropertyEmitsInvalidationFlag, DbusPropertyExplicitFlag, + NameAllowReplacementFlag, + NameQueueFlag, + NameReplaceExistingFlag, SdBus, sd_bus_open, ) @@ -50,6 +54,20 @@ def _is_property_flags_correct(flags: int) -> bool: return (0 <= num_of_flag_bits <= 1) +def _prepare_request_name_flags( + allow_replacement: bool, + replace_existing: bool, + queue: bool, +) -> int: + return ( + (NameAllowReplacementFlag if allow_replacement else 0) + + + (NameReplaceExistingFlag if replace_existing else 0) + + + (NameQueueFlag if queue else 0) + ) + + def get_default_bus() -> SdBus: try: return DEFAULT_BUS.get() @@ -65,16 +83,51 @@ def set_default_bus(new_default: SdBus) -> None: async def request_default_bus_name_async( new_name: str, - flags: int = 0,) -> None: + allow_replacement: bool = False, + replace_existing: bool = False, + queue: bool = False, +) -> None: default_bus = get_default_bus() - await default_bus.request_name_async(new_name, flags) - - -async def request_default_bus_name( + await default_bus.request_name_async( + new_name, + _prepare_request_name_flags( + allow_replacement, + replace_existing, + queue, + ) + ) + + +class _DeprecationAwaitable: + def __await__(self) -> Generator[Future[None], None, None]: + warn( + ( + 'Awaiting on request_default_bus_name' + 'is deprecated and will be removed.' + ), + DeprecationWarning, + ) + f: Future[None] = Future() + f.set_result(None) + yield from f + + +def request_default_bus_name( new_name: str, - flags: int = 0,) -> None: + allow_replacement: bool = False, + replace_existing: bool = False, + queue: bool = False, +) -> _DeprecationAwaitable: default_bus = get_default_bus() - default_bus.request_name(new_name, flags) + default_bus.request_name( + new_name, + _prepare_request_name_flags( + allow_replacement, + replace_existing, + queue, + ) + ) + return _DeprecationAwaitable() def _method_name_converter(python_name: str) -> Iterator[str]: diff --git a/test/test_deprecations.py b/test/test_deprecations.py new file mode 100644 index 0000000..702fa83 --- /dev/null +++ b/test/test_deprecations.py @@ -0,0 +1,39 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later + +# Copyright (C) 2023 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 unittest import main + +from sdbus.unittest import IsolatedDbusTestCase + +from sdbus import request_default_bus_name + + +class TestDeprecations(IsolatedDbusTestCase): + async def test_await_on_blocking_request_name(self) -> None: + with self.assertWarnsRegex( + DeprecationWarning, + 'Awaiting on request_default_bus_name' + ): + await request_default_bus_name('org.example.test') + + +if __name__ == '__main__': + main() diff --git a/test/test_request_name.py b/test/test_request_name.py index 3855fa0..26117e2 100644 --- a/test/test_request_name.py +++ b/test/test_request_name.py @@ -29,19 +29,20 @@ SdBusRequestNameExistsError, SdBusRequestNameInQueueError, ) -from sdbus.sd_bus_internals import NameQueueFlag +from sdbus.sd_bus_internals import NameAllowReplacementFlag, NameQueueFlag from sdbus.unittest import IsolatedDbusTestCase -from sdbus import sd_bus_open_user +from sdbus import ( + request_default_bus_name, + request_default_bus_name_async, + sd_bus_open_user, +) TEST_BUS_NAME = 'com.example.test' TEST_BUS_NAME_regex_match = TEST_BUS_NAME.replace('.', r'\.') -class TestRequestName(IsolatedDbusTestCase): - async def asyncSetUp(self) -> None: - await super().asyncSetUp() - +class TestRequestNameLowLevel(IsolatedDbusTestCase): def test_request_name_exception_tree(self) -> None: # Test that SdBusRequestNameError is super class # of other request name exceptions @@ -158,5 +159,51 @@ def test_name_other_error_block(self) -> None: extra_bus.request_name(TEST_BUS_NAME, 0) +class TestRequestNameBlock(IsolatedDbusTestCase): + def test_request_name_replacement(self) -> None: + extra_bus = sd_bus_open_user() + extra_bus.request_name(TEST_BUS_NAME, NameAllowReplacementFlag) + + with self.assertRaises(SdBusRequestNameExistsError): + request_default_bus_name(TEST_BUS_NAME) + + request_default_bus_name( + TEST_BUS_NAME, + replace_existing=True, + ) + + +class TestRequestNameAsync(IsolatedDbusTestCase): + async def test_request_name_replacement(self) -> None: + extra_bus = sd_bus_open_user() + await extra_bus.request_name_async( + TEST_BUS_NAME, + NameAllowReplacementFlag, + ) + + with self.assertRaises(SdBusRequestNameExistsError): + await request_default_bus_name_async(TEST_BUS_NAME) + + await request_default_bus_name_async( + TEST_BUS_NAME, + replace_existing=True, + ) + + async def test_request_name_queue(self) -> None: + extra_bus = sd_bus_open_user() + await extra_bus.request_name_async(TEST_BUS_NAME, 0) + + with self.assertRaises(SdBusRequestNameInQueueError): + await request_default_bus_name_async( + TEST_BUS_NAME, + queue=True, + ) + + extra_bus.close() + + with self.assertRaises(SdBusRequestNameAlreadyOwnerError): + await request_default_bus_name_async(TEST_BUS_NAME) + + if __name__ == '__main__': main() From 412f663eabcde36577c853a14ecc1f227a12c2ee Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sun, 15 Jan 2023 13:48:55 +0600 Subject: [PATCH 012/188] Fix typo in async property setter assertion --- src/sdbus/dbus_proxy_async_property.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sdbus/dbus_proxy_async_property.py b/src/sdbus/dbus_proxy_async_property.py index 200a859..b1a981e 100644 --- a/src/sdbus/dbus_proxy_async_property.py +++ b/src/sdbus/dbus_proxy_async_property.py @@ -225,7 +225,7 @@ def property_decorator( ) -> DbusPropertyAsync[T]: assert not iscoroutinefunction(function), ( - "Property setter can't be coroutine", + "Property getter can't be coroutine", ) new_wrapper: DbusPropertyAsync[T] = DbusPropertyAsync( From 2c4d0bffb017e2f5b1cc921fe4b25a39fe1a37db Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sun, 15 Jan 2023 16:13:34 +0600 Subject: [PATCH 013/188] Added `sdbus.utils.parse_properties_changed` function Parses the properties changed data in to a simple dictionary there keys are translated to python names and invalidated properties will have value of None. --- docs/asyncio_api.rst | 3 ++ docs/index.rst | 1 + docs/utils.rst | 22 +++++++++++ src/sdbus/dbus_common_funcs.py | 28 +++++++++++++- src/sdbus/dbus_proxy_async_interfaces.py | 23 ++++-------- src/sdbus/utils.py | 48 ++++++++++++++++++++++++ test/test_sd_bus_async.py | 45 +++++++++++++++++++++- 7 files changed, 152 insertions(+), 18 deletions(-) create mode 100644 docs/utils.rst create mode 100644 src/sdbus/utils.py diff --git a/docs/asyncio_api.rst b/docs/asyncio_api.rst index ab14b33..65556b6 100644 --- a/docs/asyncio_api.rst +++ b/docs/asyncio_api.rst @@ -75,6 +75,9 @@ Classes Signal when one of the objects properties changes. + :py:func:`sdbus.utils.parse_properties_changed` can be used to transform + this signal data in to an easier to work with dictionary. + Signal data is: Interface name : str diff --git a/docs/index.rst b/docs/index.rst index 0c8a3dd..f7eeb9b 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -38,6 +38,7 @@ If you are unfamiliar with D-Bus you might want to read following pages: asyncio_quick asyncio_api exceptions + utils examples proxies code_generator diff --git a/docs/utils.rst b/docs/utils.rst new file mode 100644 index 0000000..dd22207 --- /dev/null +++ b/docs/utils.rst @@ -0,0 +1,22 @@ +Utilities +========= + +Parsing utilities ++++++++++++++++++ + +.. py:currentmodule:: sdbus.utils + +.. py:function:: parse_properties_changed(interface, properties_changed_data, on_unknown_member='error') + + Parse data from :py:meth:`properties_changed ` signal. + + Member names will be translated to python defined names. + Invalidated properties will have a value of None. + + :param DbusInterfaceBaseAsync interface: Takes either D-Bus interface or interface class. + :param Tuple properties_changed_data: Tuple caught from signal. + :param str on_unknown_member: If an unknown D-Bus property was encountered + either raise an ``"error"`` (default), ``"ignore"`` the property + or ``"reuse"`` the D-Bus name for the member. + :returns: Dictionary of changed properties with keys translated to python + names. Invalidated properties will have value of None. diff --git a/src/sdbus/dbus_common_funcs.py b/src/sdbus/dbus_common_funcs.py index df3293d..3aa5095 100644 --- a/src/sdbus/dbus_common_funcs.py +++ b/src/sdbus/dbus_common_funcs.py @@ -22,7 +22,7 @@ from asyncio import Future, get_running_loop from contextvars import ContextVar -from typing import Generator, Iterator +from typing import Any, Dict, Generator, Iterator, Literal, Tuple from warnings import warn from .sd_bus_internals import ( @@ -159,3 +159,29 @@ def _check_sync_in_async_env() -> bool: return False except RuntimeError: return True + + +def _parse_properties_vardict( + properties_name_map: Dict[str, str], + properties_vardict: Dict[str, Tuple[str, Any]], + on_unknown_member: Literal['error', 'ignore', 'reuse'], +) -> Dict[str, Any]: + + properties_translated: Dict[str, Any] = {} + + for member_name, variant in properties_vardict.items(): + try: + python_name = properties_name_map[member_name] + except KeyError: + if on_unknown_member == 'error': + raise + elif on_unknown_member == 'ignore': + continue + elif on_unknown_member == 'reuse': + python_name = member_name + else: + raise ValueError + + properties_translated[python_name] = variant[1] + + return properties_translated diff --git a/src/sdbus/dbus_proxy_async_interfaces.py b/src/sdbus/dbus_proxy_async_interfaces.py index c7d6f9f..312b446 100644 --- a/src/sdbus/dbus_proxy_async_interfaces.py +++ b/src/sdbus/dbus_proxy_async_interfaces.py @@ -22,7 +22,7 @@ from inspect import getmembers from typing import Any, Dict, List, Literal, Optional, Tuple -from .dbus_common_funcs import get_default_bus +from .dbus_common_funcs import _parse_properties_vardict, get_default_bus from .dbus_proxy_async_interface_base import DbusInterfaceBaseAsync from .dbus_proxy_async_method import dbus_method_async from .dbus_proxy_async_property import DbusPropertyAsyncBinded @@ -94,20 +94,13 @@ async def properties_get_all_dict( dbus_properties_data = await self._properties_get_all( interface_name) - for member_name, variant in dbus_properties_data.items(): - try: - python_name = self._dbus_to_python_name_map[member_name] - except KeyError: - if on_unknown_member == 'error': - raise - elif on_unknown_member == 'ignore': - continue - elif on_unknown_member == 'reuse': - python_name = member_name - else: - raise ValueError - - properties[python_name] = variant[1] + properties.update( + _parse_properties_vardict( + self._dbus_to_python_name_map, + dbus_properties_data, + on_unknown_member, + ) + ) return properties diff --git a/src/sdbus/utils.py b/src/sdbus/utils.py new file mode 100644 index 0000000..1ada206 --- /dev/null +++ b/src/sdbus/utils.py @@ -0,0 +1,48 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later + +# Copyright (C) 2023 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 typing import Any, Dict, Literal, Type, Union + +from .dbus_common_funcs import _parse_properties_vardict +from .dbus_proxy_async_interface_base import DbusInterfaceBaseAsync +from .dbus_proxy_async_interfaces import DBUS_PROPERTIES_CHANGED_TYPING + + +def parse_properties_changed( + interface: Union[DbusInterfaceBaseAsync, Type[DbusInterfaceBaseAsync]], + properties_changed_data: DBUS_PROPERTIES_CHANGED_TYPING, + on_unknown_member: Literal['error', 'ignore', 'reuse'] = 'error', +) -> Dict[str, Any]: + changed_properties_data = properties_changed_data[1] + + for invalidated_property in properties_changed_data[2]: + changed_properties_data[invalidated_property] = ('0', None) + + return _parse_properties_vardict( + interface._dbus_to_python_name_map, + properties_changed_data[1], + on_unknown_member, + ) + + +__all__ = ( + 'parse_properties_changed', +) diff --git a/test/test_sd_bus_async.py b/test/test_sd_bus_async.py index 87aa0c2..dcda724 100644 --- a/test/test_sd_bus_async.py +++ b/test/test_sd_bus_async.py @@ -1,6 +1,6 @@ # SPDX-License-Identifier: LGPL-2.1-or-later -# Copyright (C) 2020, 2021 igo95862 +# Copyright (C) 2020-2023 igo95862 # This file is part of python-sdbus @@ -22,10 +22,11 @@ from asyncio import Event, get_running_loop, sleep, wait_for from asyncio.subprocess import create_subprocess_exec -from typing import Tuple +from typing import Tuple, cast from unittest import SkipTest from sdbus.dbus_common_funcs import PROPERTY_FLAGS_MASK, count_bits +from sdbus.dbus_proxy_async_interfaces import DBUS_PROPERTIES_CHANGED_TYPING from sdbus.exceptions import ( DbusFailedError, DbusFileExistsError, @@ -41,6 +42,7 @@ is_interface_name_valid, ) from sdbus.unittest import IsolatedDbusTestCase +from sdbus.utils import parse_properties_changed from sdbus import ( DbusInterfaceCommonAsync, @@ -823,3 +825,42 @@ async def test_empty_signal(self) -> None: self.assertIsNone(await wait_for(aw_dbus, timeout=1)) self.assertIsNone(await wait_for(q.get(), timeout=1)) + + async def test_properties_changed(self) -> None: + test_object, test_object_connection = initialize_object() + + test_str = 'should_be_emited' + + q = await test_object_connection.properties_changed._get_dbus_queue() + + async def set_property() -> None: + await test_object_connection.test_property.set_async(test_str) + + await set_property() + + properties_changed_data = cast( + DBUS_PROPERTIES_CHANGED_TYPING, + (await q.get()).get_contents(), + ) + + parsed_dict_from_class = parse_properties_changed( + TestInterface, properties_changed_data) + self.assertEqual( + test_str, + parsed_dict_from_class['test_property'], + ) + + parsed_dict_from_object = parse_properties_changed( + test_object_connection, properties_changed_data) + self.assertEqual( + test_str, + parsed_dict_from_object['test_property'], + ) + + properties_changed_data[2].append('invalidated_property') + parsed_dict_with_invalidation = parse_properties_changed( + test_object, properties_changed_data, + on_unknown_member='reuse', + ) + self.assertIsNone( + parsed_dict_with_invalidation['invalidated_property']) From d9f283d42d0bd179c4e74bda7151c9694d23f6ce Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sun, 15 Jan 2023 22:22:15 +0600 Subject: [PATCH 014/188] Added `setter_private` decorator to async properties This creates a setter that can only be called localy. Properties changed signal will be emitted to D-Bus if the setter is called localy. This is useful for properties that should be read-only from the outsude but emit signals when changed localy. --- docs/asyncio_api.rst | 12 +++++- src/sdbus/dbus_proxy_async_interface_base.py | 19 ++++++---- src/sdbus/dbus_proxy_async_property.py | 35 +++++++++++++++++- test/test_sd_bus_async.py | 39 ++++++++++++++++++++ 4 files changed, 96 insertions(+), 9 deletions(-) diff --git a/docs/asyncio_api.rst b/docs/asyncio_api.rst index 65556b6..36e44a9 100644 --- a/docs/asyncio_api.rst +++ b/docs/asyncio_api.rst @@ -344,7 +344,17 @@ Decorators Defines the setter function. This makes the property read/write instead of read-only. - See example on how to use. + See example on how to use. + + .. py:decoratormethod:: setter_private(set_function) + + Defines the private setter function. + The setter can be called locally but property + will be read-only from D-Bus. + + Calling the setter locally will emit + :py:attr:`properties_changed ` + signal to D-Bus. .. py:method:: get_async() :async: diff --git a/src/sdbus/dbus_proxy_async_interface_base.py b/src/sdbus/dbus_proxy_async_interface_base.py index cc19dac..d0294e2 100644 --- a/src/sdbus/dbus_proxy_async_interface_base.py +++ b/src/sdbus/dbus_proxy_async_interface_base.py @@ -238,18 +238,23 @@ def export_to_dbus( ) elif isinstance(dbus_something, DbusPropertyAsyncBinded): getter = dbus_something._reply_get_sync + dbus_property = dbus_something.dbus_property - setter = (dbus_something._reply_set_sync - if dbus_something.dbus_property.property_setter - is not None - else None) + if ( + dbus_property.property_setter is not None + and + dbus_property.property_setter_is_public + ): + setter = dbus_something._reply_set_sync + else: + setter = None new_interface.add_property( - dbus_something.dbus_property.property_name, - dbus_something.dbus_property.property_signature, + dbus_property.property_name, + dbus_property.property_signature, getter, setter, - dbus_something.dbus_property.flags, + dbus_property.flags, ) elif isinstance(dbus_something, DbusSignalBinded): new_interface.add_signal( diff --git a/src/sdbus/dbus_proxy_async_property.py b/src/sdbus/dbus_proxy_async_property.py index b1a981e..08631e1 100644 --- a/src/sdbus/dbus_proxy_async_property.py +++ b/src/sdbus/dbus_proxy_async_property.py @@ -74,6 +74,7 @@ def __init__( self.property_setter: Optional[ Callable[[DbusInterfaceBaseAsync, T], None]] = property_setter + self.property_setter_is_public: bool = True self.__doc__ = property_getter.__doc__ @@ -86,13 +87,27 @@ def __get__(self, def setter(self, new_set_function: Callable[ [Any, T], - None] + None], ) -> None: + assert self.property_setter is None, "Setter already defined" assert not iscoroutinefunction(new_set_function), ( "Property setter can't be coroutine", ) self.property_setter = new_set_function + def setter_private( + self, + new_set_function: Callable[ + [Any, T], + None], + ) -> None: + assert self.property_setter is None, "Setter already defined" + assert not iscoroutinefunction(new_set_function), ( + "Property setter can't be coroutine", + ) + self.property_setter = new_set_function + self.property_setter_is_public = False + class DbusPropertyAsyncBinded(DbusBindedAsync): def __init__(self, @@ -186,6 +201,24 @@ async def set_async(self, complete_object: T) -> None: self.dbus_property.property_setter( interface, complete_object) + try: + properties_changed = getattr(interface, 'properties_changed') + except AttributeError: + ... + else: + properties_changed.emit( + ( + self.dbus_property.interface_name, + { + self.dbus_property.property_name: ( + self.dbus_property.property_signature, + complete_object, + ), + }, + [] + ) + ) + return assert interface._attached_bus is not None diff --git a/test/test_sd_bus_async.py b/test/test_sd_bus_async.py index dcda724..6c205ec 100644 --- a/test/test_sd_bus_async.py +++ b/test/test_sd_bus_async.py @@ -30,6 +30,7 @@ from sdbus.exceptions import ( DbusFailedError, DbusFileExistsError, + DbusPropertyReadOnlyError, DbusUnknownObjectError, SdBusLibraryError, SdBusUnmappedMessageError, @@ -96,6 +97,7 @@ def __init__(self) -> None: self.test_string = 'test_property' self.test_string_read = 'read' self.test_no_reply_string = 'no' + self.property_private = 100 self.no_reply_sync = Event() @dbus_method_async("s", "s") @@ -132,6 +134,14 @@ def test_property_set(self, new_property: str) -> None: def test_property_read_only(self) -> str: return self.test_string_read + @dbus_property_async("x") + def test_property_private(self) -> int: + return self.property_private + + @test_property_private.setter_private + def test_private_setter(self, new_value: int) -> None: + self.property_private = new_value + @dbus_method_async("sb", "s") async def kwargs_function( self, @@ -864,3 +874,32 @@ async def set_property() -> None: ) self.assertIsNone( parsed_dict_with_invalidation['invalidated_property']) + + async def test_property_private_setter(self) -> None: + test_object, test_object_connection = initialize_object() + + new_value = 200 + self.assertNotEqual( + await test_object_connection.test_property_private, + new_value + ) + + with self.assertRaises(DbusPropertyReadOnlyError): + await test_object_connection.test_property_private.set_async( + new_value) + + q = await test_object_connection.properties_changed._get_dbus_queue() + + await test_object.test_property_private.set_async(new_value) + + self.assertEqual( + await test_object_connection.test_property_private, + new_value + ) + + changed_properties = cast( + DBUS_PROPERTIES_CHANGED_TYPING, + (await q.get()).get_contents(), + ) + + self.assertIn('TestPropertyPrivate', changed_properties[1]) From 7901764e1c0745182537db7c72d4a8778f923a04 Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sat, 21 Jan 2023 15:53:11 +0600 Subject: [PATCH 015/188] Added stability promise to README --- README.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/README.md b/README.md index d1033af..edb5487 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,18 @@ More incoming. (systemd, Bluez, screen saver... ) * [systemd](https://github.com/bernhardkaindl/python-sdbus-systemd) (by [@bernhardkaindl](https://github.com/bernhardkaindl)) +## Stability + +Python-sdbus is under development and its API is not stable. Generally +anything documented in the official documentation is considered +stable but might be deprecated. Using deprecated feature will +raise a warning and the feature will be eventually removed. + +See the [deprecations list](DEPRECATIONS.md). + +If there is a feature that is not documented but you would like to use +please open a new issue. + ## Requirements ### Binary package from PyPI From 3120faf8bdc94d2a82223d600c271ec92c34b6bd Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sat, 21 Jan 2023 22:28:09 +0600 Subject: [PATCH 016/188] Added `sdbus.utils.parse_interfaces_added` helper function Will parse `interfaces_added` signal in to path, python class and dictionaries with python member names and values. --- docs/asyncio_api.rst | 3 + docs/utils.rst | 21 +++++++ src/sdbus/utils.py | 93 ++++++++++++++++++++++++++++++- test/test_object_manager.py | 106 ++++++++++++++++++++++++++++++++++++ 4 files changed, 222 insertions(+), 1 deletion(-) diff --git a/docs/asyncio_api.rst b/docs/asyncio_api.rst index 36e44a9..c77727c 100644 --- a/docs/asyncio_api.rst +++ b/docs/asyncio_api.rst @@ -171,6 +171,9 @@ Classes Signal when a new object is added or and existing object gains a new interface. + :py:func:`sdbus.utils.parse_interfaces_added` can be used + to make signal data easier to work with. + Signal data is: Object path : str diff --git a/docs/utils.rst b/docs/utils.rst index dd22207..c574e24 100644 --- a/docs/utils.rst +++ b/docs/utils.rst @@ -20,3 +20,24 @@ Parsing utilities or ``"reuse"`` the D-Bus name for the member. :returns: Dictionary of changed properties with keys translated to python names. Invalidated properties will have value of None. + +.. py:function:: parse_interfaces_added(interfaces, interfaces_added_data, on_unknown_interface='error', on_unknown_member='error') + + Parse data from :py:meth:`interfaces_added ` signal. + + Takes an iterable of D-Bus interface classes (or a single class) and the signal data. + Returns the path of new object, the class of the added object (if it matched one of passed interface classes) + and the dictionary of python named properties and their values. + + :param Iterable[DbusInterfaceBaseAsync] interfaces: Possible interfaces that were added. + Can accept classes with multiple interfaces defined. + :param Tuple interfaces_added_data: Tuple caught from signal. + :param str on_unknown_interface: If an unknown D-Bus interface was encountered + either raise an ``"error"`` (default) or return ``"none"`` instead + of interface class. + :param str on_unknown_member: If an unknown D-Bus property was encountered + either raise an ``"error"`` (default), ``"ignore"`` the property + or ``"reuse"`` the D-Bus name for the member. + :rtype: Tuple[str, Optional[Type[DbusInterfaceBaseAsync]], Dict[str, Any]] + :returns: Path of new added object, object's class (or ``None``) and dictionary + of python translated members and their values. diff --git a/src/sdbus/utils.py b/src/sdbus/utils.py index 1ada206..9012324 100644 --- a/src/sdbus/utils.py +++ b/src/sdbus/utils.py @@ -19,7 +19,17 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA from __future__ import annotations -from typing import Any, Dict, Literal, Type, Union +from typing import ( + Any, + Dict, + FrozenSet, + Iterable, + Literal, + Optional, + Tuple, + Type, + Union, +) from .dbus_common_funcs import _parse_properties_vardict from .dbus_proxy_async_interface_base import DbusInterfaceBaseAsync @@ -43,6 +53,87 @@ def parse_properties_changed( ) +SKIP_INTERFACES = frozenset(( + 'org.freedesktop.DBus.Properties', + 'org.freedesktop.DBus.Introspectable', + 'org.freedesktop.DBus.Peer', + 'org.freedesktop.DBus.ObjectManager', +)) + + +def parse_interfaces_added( + interfaces: Union[ + Union[ + DbusInterfaceBaseAsync, + Type[DbusInterfaceBaseAsync], + ], + Iterable[ + Union[ + DbusInterfaceBaseAsync, + Type[DbusInterfaceBaseAsync], + ], + ], + ], + interfaces_added_data: Tuple[str, Dict[str, Dict[str, Any]]], + on_unknown_interface: Literal['error', 'none'] = 'error', + on_unknown_member: Literal['error', 'ignore', 'reuse'] = 'error', +) -> Tuple[str, Optional[Type[DbusInterfaceBaseAsync]], Dict[str, Any]]: + interfaces_to_class_map: Dict[ + FrozenSet[str], + Type[DbusInterfaceBaseAsync], + ] = {} + + if isinstance(interfaces, + (DbusInterfaceBaseAsync, type)): + interfaces_iter = iter((interfaces, )) + else: + interfaces_iter = iter(interfaces) + + for interface in interfaces_iter: + if ( + isinstance(interface, DbusInterfaceBaseAsync) + ): + interfaces_to_class_map[ + frozenset(interface._dbus_served_interfaces_names) + ] = type(interface) + elif ( + isinstance(interface, type) + and + issubclass(interface, DbusInterfaceBaseAsync) + ): + interfaces_to_class_map[ + frozenset(interface._dbus_served_interfaces_names) + ] = interface + else: + raise TypeError('Expected D-Bus interface, got: ', interface) + + path, properties_data = interfaces_added_data + + class_set = frozenset(properties_data.keys()) - SKIP_INTERFACES + try: + python_class = interfaces_to_class_map[class_set] + dbus_to_python_member_map = python_class._dbus_to_python_name_map + except KeyError: + if on_unknown_interface == 'error': + raise + + python_class = None + dbus_to_python_member_map = {} + + python_properties: Dict[str, Any] = {} + for _, properties in properties_data.items(): + python_properties.update( + _parse_properties_vardict( + dbus_to_python_member_map, + properties, + on_unknown_member, + ) + ) + + return path, python_class, python_properties + + __all__ = ( 'parse_properties_changed', + 'parse_interfaces_added', ) diff --git a/test/test_object_manager.py b/test/test_object_manager.py index 2e881cd..28d7526 100644 --- a/test/test_object_manager.py +++ b/test/test_object_manager.py @@ -24,6 +24,7 @@ from typing import Any, Dict, List, Tuple from sdbus.unittest import IsolatedDbusTestCase +from sdbus.utils import parse_interfaces_added from sdbus import ( DbusInterfaceCommonAsync, @@ -118,6 +119,9 @@ async def catch_interfaces_removed() -> Tuple[str, List[str]]: TEST_NUMBER, ) + with self.subTest("Test interfaces added parser"): + parse_interfaces_added(ManagedInterface, caught_added) + object_manager.remove_managed_object(managed_object) path_removed, interfaces_removed = await wait_for( @@ -138,3 +142,105 @@ def test_expot_with_no_manager(self) -> None: MANAGED_PATH, managed_object, ) + + async def test_parse_interfaces_added(self) -> None: + MANAGED_TWO_INTERFACE_NAME = MANAGED_INTERFACE_NAME + 'Two' + + class ManagedTwoInterface( + ManagedInterface, + interface_name=MANAGED_TWO_INTERFACE_NAME, + ): + + @dbus_property_async('s') + def test_str(self) -> str: + return 'test' + + loop = get_running_loop() + await self.bus.request_name_async(CONNECTION_NAME, 0) + + object_manager = DbusObjectManagerInterfaceAsync() + object_manager.export_to_dbus(OBJECT_MANAGER_PATH) + + object_manager_connection = DbusObjectManagerInterfaceAsync.new_proxy( + CONNECTION_NAME, OBJECT_MANAGER_PATH) + + async def catch_interfaces_added() -> Tuple[str, + Dict[str, + Dict[str, Any]]]: + async for x in object_manager_connection.interfaces_added: + return x + + raise RuntimeError + + catch_added_task = loop.create_task(catch_interfaces_added()) + + await sleep(0) + + managed_object = ManagedTwoInterface() + + object_manager.export_with_manager(MANAGED_PATH, managed_object) + + caught_added = await wait_for(catch_added_task, timeout=0.5) + + with self.subTest('Parse class'): + path, python_class, python_properties = ( + parse_interfaces_added(ManagedTwoInterface, caught_added) + ) + + self.assertEqual(path, MANAGED_PATH) + self.assertEqual(python_class, ManagedTwoInterface) + self.assertIn('test_str', python_properties) + self.assertIn('test_int', python_properties) + + with self.subTest('Parse object'): + path, python_class, python_properties = ( + parse_interfaces_added(managed_object, caught_added) + ) + + self.assertEqual(path, MANAGED_PATH) + self.assertEqual(python_class, ManagedTwoInterface) + self.assertIn('test_str', python_properties) + self.assertIn('test_int', python_properties) + + with self.subTest('Parse iterable'): + path, python_class, python_properties = ( + parse_interfaces_added( + (ManagedInterface, ManagedTwoInterface), + caught_added) + ) + + self.assertEqual(path, MANAGED_PATH) + self.assertEqual(python_class, ManagedTwoInterface) + self.assertIn('test_str', python_properties) + self.assertIn('test_int', python_properties) + + with self.subTest('Parse unknown'): + with self.assertRaises(KeyError): + path, python_class, python_properties = ( + parse_interfaces_added( + ManagedInterface, + caught_added) + ) + + with self.assertRaises(KeyError): + path, python_class, python_properties = ( + parse_interfaces_added( + ManagedInterface, + caught_added, + on_unknown_interface='none', + ) + ) + + path, python_class, python_properties = ( + parse_interfaces_added( + ManagedInterface, + caught_added, + on_unknown_interface='none', + on_unknown_member='reuse', + ) + ) + + self.assertEqual(path, MANAGED_PATH) + self.assertIsNone(python_class) + self.assertIn('TestStr', python_properties) + self.assertIn('TestInt', python_properties) From 157438a46edc4a6d62fd455c3a26cffb68191c67 Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sat, 21 Jan 2023 22:30:23 +0600 Subject: [PATCH 017/188] docs: Added return type to `parse_properties_changed` docs --- docs/utils.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/utils.rst b/docs/utils.rst index c574e24..d98fbca 100644 --- a/docs/utils.rst +++ b/docs/utils.rst @@ -18,6 +18,7 @@ Parsing utilities :param str on_unknown_member: If an unknown D-Bus property was encountered either raise an ``"error"`` (default), ``"ignore"`` the property or ``"reuse"`` the D-Bus name for the member. + :rtype: Dict[str, Any] :returns: Dictionary of changed properties with keys translated to python names. Invalidated properties will have value of None. From 731561d70f534d408663048ffee3607d9caf3b20 Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sat, 21 Jan 2023 23:03:58 +0600 Subject: [PATCH 018/188] Added `sdbus.utils.parse_interfaces_removed` function Parses object manager interfaces_removed signal and returns the path of removed object and python class. --- docs/asyncio_api.rst | 3 ++ docs/utils.rst | 17 +++++++ src/sdbus/utils.py | 93 +++++++++++++++++++++++++++++-------- test/test_object_manager.py | 56 +++++++++++++++++++--- 4 files changed, 143 insertions(+), 26 deletions(-) diff --git a/docs/asyncio_api.rst b/docs/asyncio_api.rst index c77727c..cc25e80 100644 --- a/docs/asyncio_api.rst +++ b/docs/asyncio_api.rst @@ -188,6 +188,9 @@ Classes Signal when existing object or and interface of existing object is removed. + :py:func:`sdbus.utils.parse_interfaces_removed` can be used + to make signal data easier to work with. + Signal data is: Object path : str diff --git a/docs/utils.rst b/docs/utils.rst index d98fbca..851315e 100644 --- a/docs/utils.rst +++ b/docs/utils.rst @@ -42,3 +42,20 @@ Parsing utilities :rtype: Tuple[str, Optional[Type[DbusInterfaceBaseAsync]], Dict[str, Any]] :returns: Path of new added object, object's class (or ``None``) and dictionary of python translated members and their values. + +.. py:function:: parse_interfaces_removed(interfaces, interfaces_removed_data, on_unknown_interface='error') + + Parse data from :py:meth:`interfaces_added ` signal. + + Takes an iterable of D-Bus interface classes (or a single class) and the signal data. + Returns the path of removed object andthe class of the added object. + (if it matched one of passed interface classes) + + :param Iterable[DbusInterfaceBaseAsync] interfaces: Possible interfaces that were removed. + Can accept classes with multiple interfaces defined. + :param Tuple interfaces_added_data: Tuple caught from signal. + :param str on_unknown_member: If an unknown D-Bus interface was encountered + either raise an ``"error"`` (default) or return ``"none"`` instead + of interface class. + :rtype: Tuple[str, Optional[Type[DbusInterfaceBaseAsync]]] + :returns: Path of removed object and object's class (or ``None``). diff --git a/src/sdbus/utils.py b/src/sdbus/utils.py index 9012324..cffb343 100644 --- a/src/sdbus/utils.py +++ b/src/sdbus/utils.py @@ -24,6 +24,7 @@ Dict, FrozenSet, Iterable, + List, Literal, Optional, Tuple, @@ -61,34 +62,19 @@ def parse_properties_changed( )) -def parse_interfaces_added( - interfaces: Union[ +def _create_interfaces_map( + interfaces_iter: Iterable[ Union[ DbusInterfaceBaseAsync, Type[DbusInterfaceBaseAsync], - ], - Iterable[ - Union[ - DbusInterfaceBaseAsync, - Type[DbusInterfaceBaseAsync], - ], - ], - ], - interfaces_added_data: Tuple[str, Dict[str, Dict[str, Any]]], - on_unknown_interface: Literal['error', 'none'] = 'error', - on_unknown_member: Literal['error', 'ignore', 'reuse'] = 'error', -) -> Tuple[str, Optional[Type[DbusInterfaceBaseAsync]], Dict[str, Any]]: + ] + ] +) -> Dict[FrozenSet[str], Type[DbusInterfaceBaseAsync]]: interfaces_to_class_map: Dict[ FrozenSet[str], Type[DbusInterfaceBaseAsync], ] = {} - if isinstance(interfaces, - (DbusInterfaceBaseAsync, type)): - interfaces_iter = iter((interfaces, )) - else: - interfaces_iter = iter(interfaces) - for interface in interfaces_iter: if ( isinstance(interface, DbusInterfaceBaseAsync) @@ -107,6 +93,35 @@ def parse_interfaces_added( else: raise TypeError('Expected D-Bus interface, got: ', interface) + return interfaces_to_class_map + + +def parse_interfaces_added( + interfaces: Union[ + Union[ + DbusInterfaceBaseAsync, + Type[DbusInterfaceBaseAsync], + ], + Iterable[ + Union[ + DbusInterfaceBaseAsync, + Type[DbusInterfaceBaseAsync], + ], + ], + ], + interfaces_added_data: Tuple[str, Dict[str, Dict[str, Any]]], + on_unknown_interface: Literal['error', 'none'] = 'error', + on_unknown_member: Literal['error', 'ignore', 'reuse'] = 'error', +) -> Tuple[str, Optional[Type[DbusInterfaceBaseAsync]], Dict[str, Any]]: + + if isinstance(interfaces, + (DbusInterfaceBaseAsync, type)): + interfaces_iter = iter((interfaces, )) + else: + interfaces_iter = iter(interfaces) + + interfaces_to_class_map = _create_interfaces_map(interfaces_iter) + path, properties_data = interfaces_added_data class_set = frozenset(properties_data.keys()) - SKIP_INTERFACES @@ -133,6 +148,44 @@ def parse_interfaces_added( return path, python_class, python_properties +def parse_interfaces_removed( + interfaces: Union[ + Union[ + DbusInterfaceBaseAsync, + Type[DbusInterfaceBaseAsync], + ], + Iterable[ + Union[ + DbusInterfaceBaseAsync, + Type[DbusInterfaceBaseAsync], + ], + ], + ], + interfaces_removed_data: Tuple[str, List[str]], + on_unknown_interface: Literal['error', 'none'] = 'error', +) -> Tuple[str, Optional[Type[DbusInterfaceBaseAsync]]]: + if isinstance(interfaces, + (DbusInterfaceBaseAsync, type)): + interfaces_iter = iter((interfaces, )) + else: + interfaces_iter = iter(interfaces) + + interfaces_to_class_map = _create_interfaces_map(interfaces_iter) + + path, interfaces_removed = interfaces_removed_data + + class_set = frozenset(interfaces_removed) - SKIP_INTERFACES + try: + python_class = interfaces_to_class_map[class_set] + except KeyError: + if on_unknown_interface == 'error': + raise + + python_class = None + + return path, python_class + + __all__ = ( 'parse_properties_changed', 'parse_interfaces_added', diff --git a/test/test_object_manager.py b/test/test_object_manager.py index 28d7526..9772baa 100644 --- a/test/test_object_manager.py +++ b/test/test_object_manager.py @@ -24,7 +24,7 @@ from typing import Any, Dict, List, Tuple from sdbus.unittest import IsolatedDbusTestCase -from sdbus.utils import parse_interfaces_added +from sdbus.utils import parse_interfaces_added, parse_interfaces_removed from sdbus import ( DbusInterfaceCommonAsync, @@ -143,7 +143,7 @@ def test_expot_with_no_manager(self) -> None: managed_object, ) - async def test_parse_interfaces_added(self) -> None: + async def test_parse_interfaces_added_removed(self) -> None: MANAGED_TWO_INTERFACE_NAME = MANAGED_INTERFACE_NAME + 'Two' class ManagedTwoInterface( @@ -174,6 +174,14 @@ async def catch_interfaces_added() -> Tuple[str, catch_added_task = loop.create_task(catch_interfaces_added()) + async def catch_interfaces_removed() -> Tuple[str, List[str]]: + async for x in object_manager_connection.interfaces_removed: + return x + + raise RuntimeError + + catch_removed_task = loop.create_task(catch_interfaces_removed()) + await sleep(0) managed_object = ManagedTwoInterface() @@ -182,7 +190,7 @@ async def catch_interfaces_added() -> Tuple[str, caught_added = await wait_for(catch_added_task, timeout=0.5) - with self.subTest('Parse class'): + with self.subTest('Parse added class'): path, python_class, python_properties = ( parse_interfaces_added(ManagedTwoInterface, caught_added) ) @@ -192,7 +200,7 @@ async def catch_interfaces_added() -> Tuple[str, self.assertIn('test_str', python_properties) self.assertIn('test_int', python_properties) - with self.subTest('Parse object'): + with self.subTest('Parse added object'): path, python_class, python_properties = ( parse_interfaces_added(managed_object, caught_added) ) @@ -202,7 +210,7 @@ async def catch_interfaces_added() -> Tuple[str, self.assertIn('test_str', python_properties) self.assertIn('test_int', python_properties) - with self.subTest('Parse iterable'): + with self.subTest('Parse added iterable'): path, python_class, python_properties = ( parse_interfaces_added( (ManagedInterface, ManagedTwoInterface), @@ -214,7 +222,7 @@ async def catch_interfaces_added() -> Tuple[str, self.assertIn('test_str', python_properties) self.assertIn('test_int', python_properties) - with self.subTest('Parse unknown'): + with self.subTest('Parse added unknown'): with self.assertRaises(KeyError): path, python_class, python_properties = ( parse_interfaces_added( @@ -244,3 +252,39 @@ async def catch_interfaces_added() -> Tuple[str, self.assertIsNone(python_class) self.assertIn('TestStr', python_properties) self.assertIn('TestInt', python_properties) + + object_manager.remove_managed_object(managed_object) + + interfaces_removed_data = await wait_for( + catch_removed_task, timeout=1) + + with self.subTest('Parse removed class'): + path, python_class = ( + parse_interfaces_removed( + ManagedTwoInterface, + interfaces_removed_data, + ) + ) + + self.assertEqual(path, MANAGED_PATH) + self.assertEqual(python_class, ManagedTwoInterface) + + with self.subTest('Parse removed unknown'): + with self.assertRaises(KeyError): + path, python_class = ( + parse_interfaces_removed( + ManagedInterface, + interfaces_removed_data, + ) + ) + + path, python_class = ( + parse_interfaces_removed( + ManagedInterface, + interfaces_removed_data, + on_unknown_interface='none', + ) + ) + + self.assertEqual(path, MANAGED_PATH) + self.assertIsNone(python_class) From 9e1f1b06b734ea33de084fbc8aabf3a25ca7e676 Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sat, 21 Jan 2023 23:08:16 +0600 Subject: [PATCH 019/188] Remove LGTM configuration LGTM is dead --- lgtm.yml | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 lgtm.yml diff --git a/lgtm.yml b/lgtm.yml deleted file mode 100644 index 3a2ccc6..0000000 --- a/lgtm.yml +++ /dev/null @@ -1,6 +0,0 @@ ---- - -extraction: - python: - python_setup: - setup_py: "./setup.py" From 89e6c40b347e177fdcf195afd2c1743c125d98e9 Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sat, 21 Jan 2023 23:28:58 +0600 Subject: [PATCH 020/188] Update CHANGELOG to version 0.11.0 --- CHANGELOG.md | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 675fc27..85a9f5d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,38 @@ +## 0.11.0 + +### Features: + +* Added support for `None` signals without data. +* Added boolean flags for the name request functions + which can be used to specify replacements or queueing. +* Added `sdbus.utils.parse_properties_changed` helper function. + Parses signal data to python member names and values. +* Added `sdbus.utils.parse_interfaces_added` helper function. + Parses signal data to path, python class and python member names + and values. +* Added `sdbus.utils.parse_interfaces_removed` helper function. + Parses signal data to path and python class. +* Added `setter_private` decorator to async properties. Private + setter can only be called locally but to D-Bus property will + appear as read only. +* Added new exceptions for when D-Bus name requests fail. + * `SdBusRequestNameExistsError`: Someone already owns name. + * `SdBusRequestNameAlreadyOwnerError`: Caller already owns name. + * `SdBusRequestNameInQueueError`: Name request queued up. + +### Deprecations: + +* Moved all exceptions to `sdbus.exceptions` module. + For backwards compatibility old exceptions will be + available from the root module until the version `1.0.0`. + +### Fixes: + +* Fixed autodoc adding `dbus_method` to dbus methods names +* Fix async D-Bus name requests not raising appropriate exceptions. +* Fixed `request_default_bus_name` being an async function. + For backwards compatibility it returns an awaitable that raises a warning. + ## 0.10.2 ### Features: From 18a16717d68b1d40d1a5f21b48a204e1b3ffbe07 Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sat, 21 Jan 2023 23:41:31 +0600 Subject: [PATCH 021/188] test: Fix `test_request_name_queue` sometimes failing If D-Bus daemon is too slow test might try to check if the name was acquired the name before bus managed to switch names. --- test/test_request_name.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/test/test_request_name.py b/test/test_request_name.py index 26117e2..e221831 100644 --- a/test/test_request_name.py +++ b/test/test_request_name.py @@ -19,7 +19,7 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA from __future__ import annotations -from asyncio import wait_for +from asyncio import get_running_loop, sleep, wait_for from unittest import main from sdbus.exceptions import ( @@ -31,6 +31,7 @@ ) from sdbus.sd_bus_internals import NameAllowReplacementFlag, NameQueueFlag from sdbus.unittest import IsolatedDbusTestCase +from sdbus_async.dbus_daemon import FreedesktopDbus from sdbus import ( request_default_bus_name, @@ -199,8 +200,25 @@ async def test_request_name_queue(self) -> None: queue=True, ) + async def catch_owner_changed() -> str: + dbus = FreedesktopDbus() + async for name, old, new in dbus.name_owner_changed: + if name != TEST_BUS_NAME: + continue + + if old and new: + return new + + raise RuntimeError + + loop = get_running_loop() + owner_changed_task = loop.create_task(catch_owner_changed()) + await sleep(0) + extra_bus.close() + await wait_for(owner_changed_task, timeout=0.5) + with self.assertRaises(SdBusRequestNameAlreadyOwnerError): await request_default_bus_name_async(TEST_BUS_NAME) From 4b739170e4f8e5140309fcb350b1b5b8f5c5fe1c Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sun, 22 Jan 2023 14:40:54 +0600 Subject: [PATCH 022/188] Update wheel dependencies --- wheel-build/build_container_archive.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wheel-build/build_container_archive.py b/wheel-build/build_container_archive.py index 9a3ebcf..53bc699 100755 --- a/wheel-build/build_container_archive.py +++ b/wheel-build/build_container_archive.py @@ -27,7 +27,7 @@ from subprocess import PIPE, run from tempfile import TemporaryDirectory -SYSTEMD_VERSION = '249.12' +SYSTEMD_VERSION = '249.14' UTIL_LINUX_VERSION = '2.37' NINJA_VERSION = '1.10.2' LIBCAP_VERSION = '2.64' From e9f3269ea7d19cc141ff5e34b5eccda50e129924 Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sun, 22 Jan 2023 14:41:14 +0600 Subject: [PATCH 023/188] Version 0.11.0 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index a3ad61f..469dbb2 100644 --- a/setup.py +++ b/setup.py @@ -93,7 +93,7 @@ def get_link_arguments() -> List[str]: 'Based on sd-bus from libsystemd.'), long_description=long_description, long_description_content_type='text/markdown', - version='0.10.2.1', + version='0.11.0', url='https://github.com/igo95862/python-sdbus', author='igo95862', author_email='igo95862@yandex.ru', From f1312d49338b8da8ecb4893b37a9db7410f697bb Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sun, 22 Jan 2023 16:34:45 +0600 Subject: [PATCH 024/188] wheel-build: Do not build systemd tests Frees up a lot of processing time building systemd. --- wheel-build/run_inside_container.py | 1 + 1 file changed, 1 insertion(+) diff --git a/wheel-build/run_inside_container.py b/wheel-build/run_inside_container.py index b69e860..be7ad59 100755 --- a/wheel-build/run_inside_container.py +++ b/wheel-build/run_inside_container.py @@ -169,6 +169,7 @@ def install_systemd() -> None: ['meson', 'setup', systemd_build_path, systemd_src_path, '-Dstatic-libsystemd=pic', + '-Dtests=false', '--buildtype', 'plain', '-Db_lto=true', '-Db_pie=true', ], From 53d16d99af39f3d43e33be0a5e9c8b1310063625 Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sun, 22 Jan 2023 18:33:36 +0600 Subject: [PATCH 025/188] Added Patreon funding page --- .github/FUNDING.yml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .github/FUNDING.yml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..8f87b46 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,2 @@ +patreon: igo95862 + From 42c49656ae7e47f4617feca0bfd716e79d2a9f42 Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sun, 22 Jan 2023 21:00:10 +0600 Subject: [PATCH 026/188] Added liberapay funding link --- .github/FUNDING.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 8f87b46..3cb7e1d 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,2 +1,2 @@ patreon: igo95862 - +liberapay: igo95862 From cc5b60f436f2f7be7ac211a577b6f2fc26d2fbc1 Mon Sep 17 00:00:00 2001 From: igo95862 Date: Tue, 7 Feb 2023 21:33:02 +0600 Subject: [PATCH 027/188] Added `modemmanager` binds URL by @zhanglongqi to README --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index edb5487..14f7c9f 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ More incoming. (systemd, Bluez, screen saver... ) ### Community interfaces * [systemd](https://github.com/bernhardkaindl/python-sdbus-systemd) (by [@bernhardkaindl](https://github.com/bernhardkaindl)) +* [modemmanager](https://github.com/zhanglongqi/python-sdbus-modemmanager) (by [@zhanglongqi](https://github.com/zhanglongqi)) ## Stability From ae7bda2aba53de25228fc0839f1afa90a357119f Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sat, 18 Mar 2023 21:12:18 +0600 Subject: [PATCH 028/188] Improve interface generator handling of multiple upper case letters Instead of converting `ACTIVATE_CONNECTION` to `a_c_t_i_v_a_t_e__c_o_n_n_e_c_t_i_o_n` convert it to `activate_connection`. --- src/sdbus/interface_generator.py | 11 +++++++++-- test/test_interface_generator.py | 21 +++++++++++++++++---- 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/src/sdbus/interface_generator.py b/src/sdbus/interface_generator.py index f5feb08..4feff43 100644 --- a/src/sdbus/interface_generator.py +++ b/src/sdbus/interface_generator.py @@ -46,9 +46,12 @@ def _camel_case_to_snake_case_generator(camel: str) -> Iterator[str]: yield first_char.lower() + last_character = first_char + # Yield every character # if upper is encountered - # yield _ and lower + # yield _ only if previous character + # was not already uppercase or underscore while True: try: c = next(i) @@ -56,11 +59,15 @@ def _camel_case_to_snake_case_generator(camel: str) -> Iterator[str]: return if c.isupper(): - yield '_' + if not last_character.isupper() and not last_character == "_": + yield '_' + yield c.lower() else: yield c + last_character = c + def camel_case_to_snake_case(camel: str) -> str: return ''.join(_camel_case_to_snake_case_generator(camel)) diff --git a/test/test_interface_generator.py b/test/test_interface_generator.py index 292d85a..51ef607 100644 --- a/test/test_interface_generator.py +++ b/test/test_interface_generator.py @@ -74,10 +74,23 @@ class TestConverter(TestCase): def test_camel_to_snake(self) -> None: - self.assertEqual( - 'activate_connection', - camel_case_to_snake_case('ActivateConnection'), - ) + with self.subTest("CamelCase"): + self.assertEqual( + 'activate_connection', + camel_case_to_snake_case('ActivateConnection'), + ) + + with self.subTest("Already snake case"): + self.assertEqual( + 'activate_connection', + camel_case_to_snake_case('activate_connection'), + ) + + with self.subTest("Upper snake case"): + self.assertEqual( + 'activate_connection', + camel_case_to_snake_case('ACTIVATE_CONNECTION'), + ) def test_interface_name_to_class(self) -> None: self.assertEqual( From 5860a6b0a0c2fafebd02023ecf7af3319a683278 Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sat, 12 Aug 2023 21:10:14 +0600 Subject: [PATCH 029/188] Use consistent D-Bus spelling for documentation D-Bus in documentation but code can use either dbus or Dbus. --- README.md | 4 +- docs/asyncio_api.rst | 56 ++++---- docs/asyncio_quick.rst | 16 +-- docs/autodoc.rst | 14 +- docs/common_api.rst | 12 +- docs/examples.rst | 2 +- docs/exceptions.rst | 18 +-- docs/general.rst | 140 +++++++++---------- docs/index.rst | 12 +- docs/sync_api.rst | 32 ++--- docs/sync_quick.rst | 16 +-- examples/simple/server.py | 2 +- src/sdbus/dbus_exceptions.py | 2 +- src/sdbus/dbus_proxy_async_interface_base.py | 2 +- src/sdbus/dbus_proxy_sync_interface_base.py | 2 +- src/sdbus/dbus_proxy_sync_property.py | 2 +- src/sdbus/interface_generator.py | 6 +- src/sdbus/sd_bus_internals_bus.c | 4 +- src/sdbus/sd_bus_internals_message.c | 2 +- src/sdbus_async/dbus_daemon/__init__.py | 14 +- src/sdbus_block/dbus_daemon/__init__.py | 14 +- 21 files changed, 186 insertions(+), 186 deletions(-) diff --git a/README.md b/README.md index 14f7c9f..f58a722 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ Features: * No Python 2 legacy. * Based on fast sd-bus from systemd. (also supports elogind) * Unified client/server interface classes. Write interface once! -* Dbus methods can have keyword and default arguments. +* D-Bus methods can have keyword and default arguments. See the [documentation](https://python-sdbus.readthedocs.io/en/latest/index.html) @@ -157,7 +157,7 @@ async def startup() -> None: # Acquire a known name on the bus # Clients will use that name to address this server await request_default_bus_name_async('org.example.test') - # Export the object to dbus + # Export the object to D-Bus export_object.export_to_dbus('/') diff --git a/docs/asyncio_api.rst b/docs/asyncio_api.rst index cc25e80..48ea831 100644 --- a/docs/asyncio_api.rst +++ b/docs/asyncio_api.rst @@ -8,8 +8,8 @@ Classes .. py:class:: DbusInterfaceCommonAsync(interface_name) - Dbus async interface class. - Dbus methods and properties should be defined using + D-Bus async interface class. + D-Bus methods and properties should be defined using :py:func:`dbus_property_async`, :py:func:`dbus_signal_async`, and :py:func:`dbus_method_async` decorators. @@ -17,19 +17,19 @@ Classes Don't forget to call ``super().__init__()`` in derived classes init calls as it sets up important attributes. - :param str interface_name: Sets the dbus interface + :param str interface_name: Sets the D-Bus interface name that will be used for all properties, methods and signals defined in the body of the class. :param bool serving_enabled: If set to :py:obj:`True` - the interface will not be served on dbus. Mostly used + the interface will not be served on D-Bus. Mostly used for interfaces that sd-bus already provides such as ``org.freedesktop.DBus.Peer``. .. py:method:: dbus_ping() :async: - Pings the remote service using dbus. + Pings the remote service using D-Bus. Useful to test if connection or remote service is alive. @@ -47,7 +47,7 @@ Classes .. py:method:: dbus_introspect() :async: - Get dbus introspection XML. + Get D-Bus introspection XML. It is users responsibility to parse that data. @@ -92,50 +92,50 @@ Classes .. py:method:: _proxify(bus, service_name, object_path) - Begin proxying to a remote dbus object. + Begin proxying to a remote D-Bus object. :param str service_name: - Remote object dbus connection name. + Remote object D-Bus connection name. For example, systemd uses ``org.freedesktop.systemd1`` :param str object_path: - Remote object dbus path. + Remote object D-Bus path. Should be a forward slash separated path. Starting object is usually ``/``. Example: ``/org/freedesktop/systemd/unit/dbus_2eservice`` :param SdBus bus: - Optional dbus connection object. - If not passed the default dbus will be used. + Optional D-Bus connection object. + If not passed the default D-Bus will be used. .. py:classmethod:: new_proxy(bus, service_name, object_path) Create new proxy object and bypass ``__init__``. :param str service_name: - Remote object dbus connection name. + Remote object D-Bus connection name. For example, systemd uses ``org.freedesktop.systemd1`` :param str object_path: - Remote object dbus path. + Remote object D-Bus path. Should be a forward slash separated path. Starting object is usually ``/``. Example: ``/org/freedesktop/systemd/unit/dbus_2eservice`` :param SdBus bus: - Optional dbus connection object. - If not passed the default dbus will be used. + Optional D-Bus connection object. + If not passed the default D-Bus will be used. .. py:method:: export_to_dbus(object_path, bus) - Object will appear and become callable on dbus. + Object will appear and become callable on D-Bus. :param str object_path: Object path that it will be available at. :param SdBus bus: - Optional dbus connection object. - If not passed the default dbus will be used. + Optional D-Bus connection object. + If not passed the default D-Bus will be used. .. py:class:: DbusObjectManagerInterfaceAsync(interface_name) @@ -218,8 +218,8 @@ Classes Object to export to D-Bus. :param SdBus bus: - Optional dbus connection object. - If not passed the default dbus will be used. + Optional D-Bus connection object. + If not passed the default D-Bus will be used. :raises RuntimeError: ObjectManager was not exported. @@ -248,11 +248,11 @@ Decorators Underlying function must be a coroutine function. - :param str input_signature: dbus input signature. + :param str input_signature: D-Bus input signature. Defaults to "" meaning method takes no arguments. Required if you intend to connect to a remote object. - :param str result_signature: dbus result signature. + :param str result_signature: D-Bus result signature. Defaults to "" meaning method returns empty reply on success. Required if you intend to serve the object. @@ -290,7 +290,7 @@ Decorators argument names will be used otherwise input arguments will be nameless - :param str method_name: Force specific dbus method name + :param str method_name: Force specific D-Bus method name instead of being based on Python function name. Example: :: @@ -318,7 +318,7 @@ Decorators .. py:decorator:: dbus_property_async(property_signature, [flags, [property_name]]) - Declare a dbus property. + Declare a D-Bus property. The underlying function has to be a regular ``def`` function. @@ -331,7 +331,7 @@ Decorators does not perform heavy IO or computation as that will block other methods or properties. - :param str property_signature: Property dbus signature. + :param str property_signature: Property D-Bus signature. Has to be a single type or container. :param int flags: modifies behavior. @@ -407,11 +407,11 @@ Decorators .. py:decorator:: dbus_signal_async([signal_signature, [signal_args_names, [flags, [signal_name]]]]) - Defines a dbus signal. + Defines a D-Bus signal. Underlying function return type hint is used for signal type hints. - :param str signal_signature: signal dbus signature. + :param str signal_signature: signal D-Bus signature. Defaults to empty signal. :param Sequence[str] signal_args_names: sequence of signal argument names. @@ -466,7 +466,7 @@ Decorators the service name of the proxy will be used. :param str bus: - Optional dbus connection object. + Optional D-Bus connection object. If not passed when called from proxy the bus connected to proxy will be used or when called from class default bus will be used. diff --git a/docs/asyncio_quick.rst b/docs/asyncio_quick.rst index 61b72e7..37ec726 100644 --- a/docs/asyncio_quick.rst +++ b/docs/asyncio_quick.rst @@ -10,8 +10,8 @@ Python-sdbus works by declaring interface classes. Interface classes for async IO should be derived from :py:class:`DbusInterfaceCommonAsync`. -The class constructor takes ``interface_name`` keyword to determine the dbus interface name for all -dbus elements declared in the class body. +The class constructor takes ``interface_name`` keyword to determine the D-Bus interface name for all +D-Bus elements declared in the class body. Example: :: @@ -77,14 +77,14 @@ Recommended to create proxy classes that a subclass of the interface: :: self._proxify('org.example.test', '/') -.. note:: Successfully initiating a proxy object does NOT guarantee that the dbus object exists. +.. note:: Successfully initiating a proxy object does NOT guarantee that the D-Bus object exists. Serving objects ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ :py:meth:`DbusInterfaceCommonAsync.export_to_dbus` method -will export the object to the dbus. After calling it the object -becomes visible on dbus for other processes to call. +will export the object to the D-Bus. After calling it the object +becomes visible on D-Bus for other processes to call. Example using ExampleInterface from before: :: @@ -238,7 +238,7 @@ Example: :: Signals ^^^^^^^^^^^^^^^^^^^^^^^^^^^ -To define a dbus signal wrap a function with :py:func:`dbus_signal_async` decorator. +To define a D-Bus signal wrap a function with :py:func:`dbus_signal_async` decorator. The function is only used for type hints information. It is recommended to just put ``raise NotImplementedError`` in to the body of the function. @@ -286,7 +286,7 @@ Example:: Subclass Overrides ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -If you define a subclass which overrides a declared dbus method or property +If you define a subclass which overrides a declared D-Bus method or property you need to use :py:func:`dbus_method_async_override` and :py:func:`dbus_property_async_override` decorators. Overridden property can decorate a new setter. @@ -318,7 +318,7 @@ Example: :: Multiple interfaces ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -A dbus object can have multiple interfaces with different methods and properties. +A D-Bus object can have multiple interfaces with different methods and properties. To implement this define multiple interface classes and do a multiple inheritance on all interfaces the object has. diff --git a/docs/autodoc.rst b/docs/autodoc.rst index accf77c..1fbc8e4 100644 --- a/docs/autodoc.rst +++ b/docs/autodoc.rst @@ -2,7 +2,7 @@ Autodoc extensions ================== Python-sdbus has an extension for Sphinx autodoc that can -document dbus interfaces. +document D-Bus interfaces. To use it include ``"sdbus.autodoc"`` extension in your ``conf.py`` file. @@ -22,16 +22,16 @@ uses it to document the classes. .. warning:: Autodoc extension is early in development and has multiple issues. For example, the inheritance ``:inherited-members:`` - does not work on the dbus elements. + does not work on the D-Bus elements. Writing docstrings ------------------- -The dbus methods should be documented same way as the regular function +The D-Bus methods should be documented same way as the regular function would. See `Sphinx documentation on possible fields \ `_ -Example docstring for a dbus method: +Example docstring for a D-Bus method: .. code-block:: python @@ -45,14 +45,14 @@ Example docstring for a dbus method: """ raise NotImplementedError -Dbus properties and signals will be annotated with type taken from the +D-Bus properties and signals will be annotated with type taken from the stub function. .. code-block:: python @dbus_property_async('as') def features(self) -> List[str]: - """List of dbus daemon features. + """List of D-Bus daemon features. Features include: @@ -61,7 +61,7 @@ stub function. header fields. * 'SELinux' - Messages filtered by SELinux on this bus. * 'SystemdActivation' - services activated by systemd if their \ - .service file specifies a dbus name. + .service file specifies a D-Bus name. """ raise NotImplementedError diff --git a/docs/common_api.rst b/docs/common_api.rst index b19d183..a604447 100644 --- a/docs/common_api.rst +++ b/docs/common_api.rst @@ -5,7 +5,7 @@ These calls are shared between async and blocking API. .. py:currentmodule:: sdbus -Dbus connections calls +D-Bus connections calls ++++++++++++++++++++++++++++++++++ .. py:function:: request_default_bus_name_async(new_name, allow_replacement, replace_existing, queue) @@ -14,9 +14,9 @@ Dbus connections calls Acquire a name on the default bus async. :param str new_name: the name to acquire. - Must be a valid dbus service name. + Must be a valid D-Bus service name. :param str new_name: the name to acquire. - Must be a valid dbus service name. + Must be a valid D-Bus service name. :param bool allow_replacement: If name was acquired allow other peers to take away the name. :param bool replace_existing: If current name owner allows, take @@ -32,7 +32,7 @@ Dbus connections calls Acquire a name on the default bus. :param str new_name: the name to acquire. - Must be a valid dbus service name. + Must be a valid D-Bus service name. :param bool allow_replacement: If name was acquired allow other peers to take away the name. :param bool replace_existing: If current name owner allows, take @@ -120,13 +120,13 @@ Helper functions :return: valid object path :rtype: str - Example on how systemd encodes unit names on dbus: :: + Example on how systemd encodes unit names on D-Bus: :: from sdbus import encode_object_path # System uses /org/freedesktop/systemd1/unit as prefix of all units - # dbus.service is a name of dbus unit but dot . is not a valid object path + # dbus.service is a name of D-Bus unit but dot . is not a valid object path s = encode_object_path('/org/freedesktop/systemd1/unit', 'dbus.service') print(s) # Prints: /org/freedesktop/systemd1/unit/dbus_2eservice diff --git a/docs/examples.rst b/docs/examples.rst index 7ef60c2..f4bc9c3 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -74,7 +74,7 @@ There are 3 files: # Acquire a known name on the bus # Clients will use that name to address to this server await request_default_bus_name_async('org.example.test') - # Export the object to dbus + # Export the object to D-Bus export_object.export_to_dbus('/') diff --git a/docs/exceptions.rst b/docs/exceptions.rst index 2be6f24..35d2bd2 100644 --- a/docs/exceptions.rst +++ b/docs/exceptions.rst @@ -6,7 +6,7 @@ Exceptions Error name bound exceptions +++++++++++++++++++++++++++++++ -These exceptions are bound to specific dbus error names. For example, +These exceptions are bound to specific D-Bus error names. For example, :py:exc:`DbusFailedError` is bound to `org.freedesktop.DBus.Error.Failed` error name. @@ -230,7 +230,7 @@ Error name exception list No network access. - Encountered you use Dbus over TCP or SSH. + Encountered you use D-Bus over TCP or SSH. .. py:attribute:: dbus_error_name :type: str @@ -280,7 +280,7 @@ Error name exception list .. py:exception:: DbusUnknownMethodError - Unknown dbus method. + Unknown D-Bus method. .. py:attribute:: dbus_error_name :type: str @@ -288,7 +288,7 @@ Error name exception list .. py:exception:: DbusUnknownObjectError - Unknown dbus object. + Unknown D-Bus object. .. py:attribute:: dbus_error_name :type: str @@ -296,7 +296,7 @@ Error name exception list .. py:exception:: DbusUnknownInterfaceError - Unknown dbus interface. + Unknown D-Bus interface. .. py:attribute:: dbus_error_name :type: str @@ -304,7 +304,7 @@ Error name exception list .. py:exception:: DbusUnknownPropertyError - Unknown dbus property. + Unknown D-Bus property. .. py:attribute:: dbus_error_name :type: str @@ -312,7 +312,7 @@ Error name exception list .. py:exception:: DbusPropertyReadOnlyError - Dbus property is read only. + D-Bus property is read only. .. py:attribute:: dbus_error_name :type: str @@ -328,7 +328,7 @@ Error name exception list .. py:exception:: DbusInvalidSignatureError - Invalid dbus type signature. + Invalid D-Bus type signature. .. py:attribute:: dbus_error_name :type: str @@ -344,7 +344,7 @@ Error name exception list .. py:exception:: DbusInconsistentMessageError - Dbus message is malformed. + D-Bus message is malformed. .. py:attribute:: dbus_error_name :type: str diff --git a/docs/general.rst b/docs/general.rst index d4c72be..a9d9c4d 100644 --- a/docs/general.rst +++ b/docs/general.rst @@ -17,15 +17,15 @@ Asyncio is a part of python standard library that allows non-blocking io. `Asyncio documentation `_ Generally blocking IO should only be used for simple scripts and programs that interact -with existing dbus objects. +with existing D-Bus objects. Blocking: ^^^^^^^^^^^^^^^^^^^^^ * Blocking is easier to initiate (no event loop) * Properties behave exactly as Python properties do. (i.e. can assign with '=' operator) * Only allows one request at a time. -* No dbus signals. -* Cannot serve objects, only interact with existing object on dbus. +* No D-Bus signals. +* Cannot serve objects, only interact with existing object on D-Bus. :doc:`/sync_quick` @@ -35,8 +35,8 @@ Asyncio: ^^^^^^^^^^^^^^^^^^^^^^^^ * Calls need to be ``await`` ed. * Multiple requests at the same time. -* Serve object on dbus for other programs. -* Dbus Signals. +* Serve object on D-Bus for other programs. +* D-Bus Signals. :doc:`/asyncio_quick` @@ -44,12 +44,12 @@ Asyncio: .. _dbus-types: -Dbus types conversion +D-Bus types conversion ++++++++++++++++++++++++ -`Dbus types reference `_ +`D-Bus types reference `_ -.. note:: Python integers are unlimited size but dbus integers are not. +.. note:: Python integers are unlimited size but D-Bus integers are not. All integer types raise :py:exc:`OverflowError` if you try to pass number outside the type size. @@ -58,74 +58,74 @@ Dbus types conversion Signed integers range is ``-(2**(bit_size-1)) < (2**(bit_size-1))-1``. -+-------------+----------+-----------------+--------------------------------------------------------------------+ -| Name | Dbus type| Python type | Description | -+=============+==========+=================+====================================================================+ -| Boolean | b | :py:obj:`bool` | :py:obj:`True` or :py:obj:`False` | -+-------------+----------+-----------------+--------------------------------------------------------------------+ -| Byte | y | :py:obj:`int` | Unsigned 8-bit integer. | -| | | | **Note:** array of bytes (*ay*) has different type | -| | | | in python domain. | -+-------------+----------+-----------------+--------------------------------------------------------------------+ -| Int16 | n | :py:obj:`int` | Signed 16-bit integer. | -+-------------+----------+-----------------+--------------------------------------------------------------------+ -| Uint16 | q | :py:obj:`int` | Unsigned 16-bit integer. | -+-------------+----------+-----------------+--------------------------------------------------------------------+ -| Int32 | i | :py:obj:`int` | Signed 32-bit integer. | -+-------------+----------+-----------------+--------------------------------------------------------------------+ -| Uint32 | u | :py:obj:`int` | Unsigned 32-bit integer. | -+-------------+----------+-----------------+--------------------------------------------------------------------+ -| Int64 | x | :py:obj:`int` | Signed 64-bit integer. | -+-------------+----------+-----------------+--------------------------------------------------------------------+ -| Uint64 | t | :py:obj:`int` | Unsigned 64-bit integer. | -+-------------+----------+-----------------+--------------------------------------------------------------------+ -| Double | d | :py:obj:`float` | Float point number | -+-------------+----------+-----------------+--------------------------------------------------------------------+ -| Unix FD | h | :py:obj:`int` | File descriptor | -+-------------+----------+-----------------+--------------------------------------------------------------------+ -| String | s | :py:obj:`str` | String | -+-------------+----------+-----------------+--------------------------------------------------------------------+ -| Object | o | :py:obj:`str` | Syntactically correct dbus object path | -| Path | | | | -+-------------+----------+-----------------+--------------------------------------------------------------------+ -| Signature | g | :py:obj:`str` | Dbus type signature | -+-------------+----------+-----------------+--------------------------------------------------------------------+ -| Array | a | :py:obj:`list` | List of some single type. | -| | | | | -| | | | Example: ``as`` array of strings | -+-------------+----------+-----------------+--------------------------------------------------------------------+ -| Byte Array | ay | :py:obj:`bytes` | Array of bytes. Not a unique type in dbus but a different type in | -| | | | Python. Accepts both :py:obj:`bytes` and :py:obj:`bytearray`. | -| | | | Used for binary data. | -+-------------+----------+-----------------+--------------------------------------------------------------------+ -| Struct | () | :py:obj:`tuple` | Tuple. | -| | | | | -| | | | Example: ``(isax)`` tuple of int, string and array of int. | -+-------------+----------+-----------------+--------------------------------------------------------------------+ -| Dictionary | a{} | :py:obj:`dict` | Dictionary with key type and value type. | -| | | | | -| | | | **Note:** Dictionary is always a part of array. | -| | | | I.E. ``a{si}`` is the dict with string keys and integer values. | -| | | | ``{si}`` is NOT a valid signature. | -+-------------+----------+-----------------+--------------------------------------------------------------------+ -| Variant | v | :py:obj:`tuple` | Unknown type that can be any single type. | -| | | | In Python represented by a tuple of | -| | | | a signature string and a single type. | -| | | | | -| | | | Example: ``("s", "test")`` variant of a single string | -+-------------+----------+-----------------+--------------------------------------------------------------------+ ++-------------+------------+-----------------+--------------------------------------------------------------------+ +| Name | D-Bus type | Python type | Description | ++=============+============+=================+====================================================================+ +| Boolean | b | :py:obj:`bool` | :py:obj:`True` or :py:obj:`False` | ++-------------+------------+-----------------+--------------------------------------------------------------------+ +| Byte | y | :py:obj:`int` | Unsigned 8-bit integer. | +| | | | **Note:** array of bytes (*ay*) has different type | +| | | | in python domain. | ++-------------+------------+-----------------+--------------------------------------------------------------------+ +| Int16 | n | :py:obj:`int` | Signed 16-bit integer. | ++-------------+------------+-----------------+--------------------------------------------------------------------+ +| Uint16 | q | :py:obj:`int` | Unsigned 16-bit integer. | ++-------------+------------+-----------------+--------------------------------------------------------------------+ +| Int32 | i | :py:obj:`int` | Signed 32-bit integer. | ++-------------+------------+-----------------+--------------------------------------------------------------------+ +| Uint32 | u | :py:obj:`int` | Unsigned 32-bit integer. | ++-------------+------------+-----------------+--------------------------------------------------------------------+ +| Int64 | x | :py:obj:`int` | Signed 64-bit integer. | ++-------------+------------+-----------------+--------------------------------------------------------------------+ +| Uint64 | t | :py:obj:`int` | Unsigned 64-bit integer. | ++-------------+------------+-----------------+--------------------------------------------------------------------+ +| Double | d | :py:obj:`float` | Float point number | ++-------------+------------+-----------------+--------------------------------------------------------------------+ +| Unix FD | h | :py:obj:`int` | File descriptor | ++-------------+------------+-----------------+--------------------------------------------------------------------+ +| String | s | :py:obj:`str` | String | ++-------------+------------+-----------------+--------------------------------------------------------------------+ +| Object | o | :py:obj:`str` | Syntactically correct D-Bus object path | +| Path | | | | ++-------------+------------+-----------------+--------------------------------------------------------------------+ +| Signature | g | :py:obj:`str` | D-Bus type signature | ++-------------+------------+-----------------+--------------------------------------------------------------------+ +| Array | a | :py:obj:`list` | List of some single type. | +| | | | | +| | | | Example: ``as`` array of strings | ++-------------+------------+-----------------+--------------------------------------------------------------------+ +| Byte Array | ay | :py:obj:`bytes` | Array of bytes. Not a unique type in D-Bus but a different type in | +| | | | Python. Accepts both :py:obj:`bytes` and :py:obj:`bytearray`. | +| | | | Used for binary data. | ++-------------+------------+-----------------+--------------------------------------------------------------------+ +| Struct | () | :py:obj:`tuple` | Tuple. | +| | | | | +| | | | Example: ``(isax)`` tuple of int, string and array of int. | ++-------------+------------+-----------------+--------------------------------------------------------------------+ +| Dictionary | a{} | :py:obj:`dict` | Dictionary with key type and value type. | +| | | | | +| | | | **Note:** Dictionary is always a part of array. | +| | | | I.E. ``a{si}`` is the dict with string keys and integer values. | +| | | | ``{si}`` is NOT a valid signature. | ++-------------+------------+-----------------+--------------------------------------------------------------------+ +| Variant | v | :py:obj:`tuple` | Unknown type that can be any single type. | +| | | | In Python represented by a tuple of | +| | | | a signature string and a single type. | +| | | | | +| | | | Example: ``("s", "test")`` variant of a single string | ++-------------+------------+-----------------+--------------------------------------------------------------------+ Name conversions +++++++++++++++++++++ -Dbus uses CamelCase for method names. +D-Bus uses CamelCase for method names. Python uses snake_case. When decorating a method name will be automatically translated from snake_case to CamelCase. Example: ``close_notification`` -> ``CloseNotification`` -However, all decorators have a parameter to force Dbus name to a specific value. +However, all decorators have a parameter to force D-Bus name to a specific value. See API documentation for a particular decorator. @@ -154,14 +154,14 @@ new bus connections. Glossary +++++++++++++++++++++ -* **Bus** object representing connection to dbus. -* **Proxy** Python object that represents an object on DBus. +* **Bus** object representing connection to D-Bus. +* **Proxy** Python object that represents an object on D-Bus. Without proxy you manipulate messages directly. * **Remote** something that exists outside current Python process. * **Local** something that exists inside current Python scope. -* **Service Name** a well known name that an process can acquire on dbus. +* **Service Name** a well known name that an process can acquire on D-Bus. For example, systemd acquires ``org.freedesktop.systemd1`` name. -* **Signature** dbus type definition. Represented by a string. See :ref:`dbus-types`. +* **Signature** D-Bus type definition. Represented by a string. See :ref:`dbus-types`. Contents ++++++++++++++++++++ diff --git a/docs/index.rst b/docs/index.rst index f7eeb9b..1baa6b7 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,30 +1,30 @@ Welcome to Python-sdbus documentation! ======================================================= -Python-sdbus is the python dbus library that aim to use the modern features of python +Python-sdbus is the python D-Bus library that aim to use the modern features of python * `Asyncio `_ * `Type hints `_ * `Based on fast sd-bus `_ * Unified client/server interface classes. Write interface class once. -* Dbus methods can have keyword and default arguments. +* D-Bus methods can have keyword and default arguments. D-Bus ----------- D-Bus is the inter-process communication standard commonly used on Linux desktop. -This documentation expects you to be familiar with dbus concepts and conventions. +This documentation expects you to be familiar with D-Bus concepts and conventions. If you are unfamiliar with D-Bus you might want to read following pages: `Wikipedia page `_ -`Lennart Poettering post about dbus `_ +`Lennart Poettering post about D-Bus `_ -`Dbus specification by freedesktop `_ +`D-Bus specification by freedesktop.org `_ -`Install D-Feet D-Bus debugger and observe services and objects on your dbus `_ +`Install D-Feet D-Bus debugger and observe services and objects on your D-Bus `_ .. toctree:: diff --git a/docs/sync_api.rst b/docs/sync_api.rst index a3ce8e8..41a27c9 100644 --- a/docs/sync_api.rst +++ b/docs/sync_api.rst @@ -8,11 +8,11 @@ Classes .. py:class:: DbusInterfaceCommon(interface_name) - Dbus interface class. - Dbus methods and properties should be defined using + D-Bus interface class. + D-Bus methods and properties should be defined using :py:func:`dbus_property` and :py:func:`dbus_method` decorators. - :param str interface_name: Sets the dbus interface + :param str interface_name: Sets the D-Bus interface name that will be used for all properties and methods defined in the body of the class @@ -21,22 +21,22 @@ Classes Init will create a proxy to a remote object :param str service_name: - Remote object dbus connection name. + Remote object D-Bus connection name. For example, systemd uses ``org.freedesktop.systemd1`` :param str object_path: - Remote object dbus path. + Remote object D-Bus path. Should be a forward slash separated path. Starting object is usually ``/``. Example: ``/org/freedesktop/systemd/unit/dbus_2eservice`` :param SdBus bus: - Optional dbus connection object. - If not passed the default dbus will be used. + Optional D-Bus connection object. + If not passed the default D-Bus will be used. .. py:method:: dbus_ping() - Pings the remote service using dbus. + Pings the remote service using D-Bus. Useful to test if connection or remote service is alive. @@ -52,7 +52,7 @@ Classes .. py:method:: dbus_introspect() - Get dbus introspection XML. + Get D-Bus introspection XML. It is users responsibility to parse that data. @@ -132,13 +132,13 @@ Decorators +++++++++++++++ .. py:decorator:: dbus_method([input_signature, [flags, [method_name]]]) - - Define dbus method - Decorated function becomes linked to dbus method. + Define D-Bus method + + Decorated function becomes linked to D-Bus method. Always use round brackets () even when not passing any arguments. - :param str input_signature: dbus input signature. + :param str input_signature: D-Bus input signature. Defaults to "" meaning method takes no arguments. Required if method takes any arguments. @@ -193,16 +193,16 @@ Decorators .. py:decorator:: dbus_property([property_signature, [flags, [property_name]]]) - Define dbus property + Define D-Bus property Property works just like @property decorator would. Always use round brackets () even when not passing any arguments. - Read only property can be indicated by passing empty dbus signature "". + Read only property can be indicated by passing empty D-Bus signature "". Trying to assign a read only property will raise :py:exc:`AttributeError` - :param str property_signature: dbus property signature. + :param str property_signature: D-Bus property signature. Empty signature "" indicates read-only property. Defaults to empty signature "". Required only for writable properties. diff --git a/docs/sync_quick.rst b/docs/sync_quick.rst index f28eac9..8fd79c6 100644 --- a/docs/sync_quick.rst +++ b/docs/sync_quick.rst @@ -10,8 +10,8 @@ Python-sdbus works by declaring interface classes. Interface classes for blocking IO should be derived from :py:class:`DbusInterfaceCommon`. -The class constructor takes ``interface_name`` keyword to determine the dbus interface name for all -dbus elements declared in the class body. +The class constructor takes ``interface_name`` keyword to determine the D-Bus interface name for all +D-Bus elements declared in the class body. Example:: @@ -42,13 +42,13 @@ Example:: def test_int(self) -> int: raise NotImplementedError -This is an interface of that defines a one dbus method and one property. +This is an interface of that defines a one D-Bus method and one property. The actual body of the decorated function will not be called. Instead the call will be routed -through dbus to a another process. Interface can have non-decorated functions that will act +through D-Bus to a another process. Interface can have non-decorated functions that will act as regular methods. -Blocking IO can only interact with existing dbus objects and can not be +Blocking IO can only interact with existing D-Bus objects and can not be served for other processes to interact with. See :ref:`blocking-vs-async` Initiating proxy @@ -68,7 +68,7 @@ Example creating a proxy and calling method:: d.close_notification(1234) -.. note:: Successfully initiating a proxy object does NOT guarantee that the dbus object +.. note:: Successfully initiating a proxy object does NOT guarantee that the D-Bus object exists. Methods @@ -98,7 +98,7 @@ Example: :: Properties ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -DBus property is defined by wrapping a function with :py:func:`dbus_property` decorator. +D-Bus property is defined by wrapping a function with :py:func:`dbus_property` decorator. Example: :: @@ -133,7 +133,7 @@ If property is read-only when :py:exc:`.DbusPropertyReadOnlyError` will be raise Multiple interfaces ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -A dbus object can have multiple interfaces with different methods and properties. +A D-Bus object can have multiple interfaces with different methods and properties. To implement this define multiple interface classes and do a multiple inheritance on all interfaces the object has. diff --git a/examples/simple/server.py b/examples/simple/server.py index b5306a7..cc7d73d 100644 --- a/examples/simple/server.py +++ b/examples/simple/server.py @@ -48,7 +48,7 @@ async def startup() -> None: # Acquire a known name on the bus # Clients will use that name to address this server await request_default_bus_name_async('org.example.test') - # Export the object to dbus + # Export the object to D-Bus export_object.export_to_dbus('/') diff --git a/src/sdbus/dbus_exceptions.py b/src/sdbus/dbus_exceptions.py index afc9358..a78f1fb 100644 --- a/src/sdbus/dbus_exceptions.py +++ b/src/sdbus/dbus_exceptions.py @@ -38,7 +38,7 @@ def __new__(cls, name: str, dbus_error_name = namespace.get('dbus_error_name') if dbus_error_name is None: - raise TypeError('Dbus error name not passed') + raise TypeError('D-Bus error name not passed') new_cls = super().__new__(cls, name, bases, namespace) diff --git a/src/sdbus/dbus_proxy_async_interface_base.py b/src/sdbus/dbus_proxy_async_interface_base.py index d0294e2..0a3dd7c 100644 --- a/src/sdbus/dbus_proxy_async_interface_base.py +++ b/src/sdbus/dbus_proxy_async_interface_base.py @@ -114,7 +114,7 @@ def __new__(cls, name: str, else: if not isinstance(value, DbusOverload): raise TypeError( - "Attempted to overload dbus definition" + "Attempted to overload D-Bus definition" " without using @dbus_overload decorator" ) diff --git a/src/sdbus/dbus_proxy_sync_interface_base.py b/src/sdbus/dbus_proxy_sync_interface_base.py index ca7e737..eb8d5a0 100644 --- a/src/sdbus/dbus_proxy_sync_interface_base.py +++ b/src/sdbus/dbus_proxy_sync_interface_base.py @@ -74,7 +74,7 @@ def __new__(cls, name: str, ) for key in super_declared_interfaces & namespace.keys(): - raise TypeError("Attempted to overload dbus definition" + raise TypeError("Attempted to overload D-Bus definition" " blocking interfaces do not support overloading") namespace['_dbus_served_interfaces_names'] = \ diff --git a/src/sdbus/dbus_proxy_sync_property.py b/src/sdbus/dbus_proxy_sync_property.py index 7da6e51..e0167f7 100644 --- a/src/sdbus/dbus_proxy_sync_property.py +++ b/src/sdbus/dbus_proxy_sync_property.py @@ -98,7 +98,7 @@ def __set__(self, obj: DbusInterfaceBase, value: T) -> None: ) if not self.property_signature: - raise AttributeError('Dbus property is read only') + raise AttributeError('D-Bus property is read only') assert obj._attached_bus is not None assert obj._remote_service_name is not None diff --git a/src/sdbus/interface_generator.py b/src/sdbus/interface_generator.py index 4feff43..402439a 100644 --- a/src/sdbus/interface_generator.py +++ b/src/sdbus/interface_generator.py @@ -347,7 +347,7 @@ def typing(self) -> str: return DbusSigToTyping.typing_complete(self.dbus_type) def __repr__(self) -> str: - return (f"Dbus Arg: {self.name}, " + return (f"D-Bus Arg: {self.name}, " f"type: {self.dbus_type}, " f"is input: {self.is_input}") @@ -412,7 +412,7 @@ def result_typing(self) -> str: [x.dbus_type for x in self.result_args]) def __repr__(self) -> str: - return (f"Dbus Method: {self.method_name}, " + return (f"D-Bus Method: {self.method_name}, " f"args: {self.args_names_and_typing}, " f"result: {self.dbus_result_signature}") @@ -538,7 +538,7 @@ def __init__(self, element: Element): else: ... else: - raise ValueError(f'Unknown dbus member {dbus_member}') + raise ValueError(f'Unknown D-Bus member {dbus_member}') def generate_interface_class(self) -> str: from jinja2 import Environment as JinjaEnv diff --git a/src/sdbus/sd_bus_internals_bus.c b/src/sdbus/sd_bus_internals_bus.c index 8597858..8882ae2 100644 --- a/src/sdbus/sd_bus_internals_bus.c +++ b/src/sdbus/sd_bus_internals_bus.c @@ -659,8 +659,8 @@ static PyMethodDef SdBus_methods[] = { {"get_signal_queue_async", (SD_BUS_PY_FUNC_TYPE)SdBus_get_signal_queue, SD_BUS_PY_METH, "Returns a future that returns a queue that queues signal " "messages"}, - {"request_name_async", (SD_BUS_PY_FUNC_TYPE)SdBus_request_name_async, SD_BUS_PY_METH, "Request dbus name async"}, - {"request_name", (SD_BUS_PY_FUNC_TYPE)SdBus_request_name, SD_BUS_PY_METH, "Request dbus name blocking"}, + {"request_name_async", (SD_BUS_PY_FUNC_TYPE)SdBus_request_name_async, SD_BUS_PY_METH, "Request D-Bus name async"}, + {"request_name", (SD_BUS_PY_FUNC_TYPE)SdBus_request_name, SD_BUS_PY_METH, "Request D-Bus name blocking"}, {"add_object_manager", (SD_BUS_PY_FUNC_TYPE)SdBus_add_object_manager, SD_BUS_PY_METH, "Add object manager at the path"}, {"emit_object_added", (SD_BUS_PY_FUNC_TYPE)SdBus_emit_object_added, SD_BUS_PY_METH, "Emit signal that object was added"}, {"emit_object_removed", (SD_BUS_PY_FUNC_TYPE)SdBus_emit_object_removed, SD_BUS_PY_METH, "Emit signal that object was removed"}, diff --git a/src/sdbus/sd_bus_internals_message.c b/src/sdbus/sd_bus_internals_message.c index d6f7652..f6e4fb9 100644 --- a/src/sdbus/sd_bus_internals_message.c +++ b/src/sdbus/sd_bus_internals_message.c @@ -577,7 +577,7 @@ static PyObject* _parse_complete(PyObject* complete_obj, _Parse_state* parser_st } case '{': { // Dict - PyErr_SetString(PyExc_TypeError, "Dbus dict can't be outside of array"); + PyErr_SetString(PyExc_TypeError, "D-Bus dict can't be outside of array"); return NULL; break; } diff --git a/src/sdbus_async/dbus_daemon/__init__.py b/src/sdbus_async/dbus_daemon/__init__.py index 0816d03..69c177e 100644 --- a/src/sdbus_async/dbus_daemon/__init__.py +++ b/src/sdbus_async/dbus_daemon/__init__.py @@ -35,15 +35,15 @@ class FreedesktopDbus(DbusInterfaceCommonAsync, """D-Bus daemon.""" def __init__(self, bus: Optional[SdBus] = None): - """This is the dbus daemon interface. Used for querying dbus state. + """This is the D-Bus daemon interface. Used for querying D-Bus state. - Dbus interface object path and service name is + D-Bus interface object path and service name is predetermined. (at ``'org.freedesktop.DBus'``, ``'/org/freedesktop/DBus'``) :param SdBus bus: - Optional dbus connection. - If not passed the default dbus will be used. + Optional D-Bus connection. + If not passed the default D-Bus will be used. """ super().__init__() self._proxify( @@ -135,7 +135,7 @@ async def start_service_by_name( @dbus_property_async('as') def features(self) -> List[str]: - """List of dbus daemon features. + """List of D-Bus daemon features. Features include: @@ -144,13 +144,13 @@ def features(self) -> List[str]: header fields. * 'SELinux' - Messages filtered by SELinux on this bus. * 'SystemdActivation' - services activated by systemd if their \ - .service file specifies a dbus name. + .service file specifies a D-Bus name. """ raise NotImplementedError @dbus_property_async('as') def interfaces(self) -> List[str]: - """Extra dbus daemon interfaces""" + """Extra D-Bus daemon interfaces""" raise NotImplementedError @dbus_signal_async('s') diff --git a/src/sdbus_block/dbus_daemon/__init__.py b/src/sdbus_block/dbus_daemon/__init__.py index 39ca578..1b321cd 100644 --- a/src/sdbus_block/dbus_daemon/__init__.py +++ b/src/sdbus_block/dbus_daemon/__init__.py @@ -29,15 +29,15 @@ class FreedesktopDbus(DbusInterfaceCommon, """D-Bus daemon.""" def __init__(self, bus: Optional[SdBus] = None): - """This is the dbus daemon interface. Used for querying dbus state. + """This is the D-Bus daemon interface. Used for querying D-Bus state. - Dbus interface object path and service name is + D-Bus interface object path and service name is predetermined. (at ``'org.freedesktop.DBus'``, ``'/org/freedesktop/DBus'``) :param SdBus bus: - Optional dbus connection. - If not passed the default dbus will be used. + Optional D-Bus connection. + If not passed the default D-Bus will be used. """ super().__init__( 'org.freedesktop.DBus', @@ -126,7 +126,7 @@ def start_service_by_name( @dbus_property('as') def features(self) -> List[str]: - """List of dbus daemon features. + """List of D-Bus daemon features. Features include: @@ -135,11 +135,11 @@ def features(self) -> List[str]: header fields. * 'SELinux' - Messages filtered by SELinux on this bus. * 'SystemdActivation' - services activated by systemd if their \ - .service file specifies a dbus name. + .service file specifies a D-Bus name. """ raise NotImplementedError @dbus_property('as') def interfaces(self) -> List[str]: - """Extra dbus daemon interfaces""" + """Extra D-Bus daemon interfaces""" raise NotImplementedError From 1399e019634ce936434bc580c441b217b82f1826 Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sat, 12 Aug 2023 22:00:03 +0600 Subject: [PATCH 030/188] Use PyDoc_STR macro for C module docstrings Some Python builds might disable the docstrings controlled by this macro. --- src/sdbus/sd_bus_internals.c | 2 +- src/sdbus/sd_bus_internals_bus.c | 39 ++++++++++++-------------- src/sdbus/sd_bus_internals_funcs.c | 32 ++++++++++----------- src/sdbus/sd_bus_internals_interface.c | 8 +++--- src/sdbus/sd_bus_internals_message.c | 35 ++++++++++++----------- 5 files changed, 57 insertions(+), 59 deletions(-) diff --git a/src/sdbus/sd_bus_internals.c b/src/sdbus/sd_bus_internals.c index 670a782..0eb4a02 100644 --- a/src/sdbus/sd_bus_internals.c +++ b/src/sdbus/sd_bus_internals.c @@ -70,7 +70,7 @@ PyType_Spec SdBusSlotType = { }; static PyModuleDef sd_bus_internals_module = { - PyModuleDef_HEAD_INIT, .m_name = "sd_bus_internals", .m_doc = "Sd bus internals module.", .m_methods = SdBusPyInternal_methods, .m_size = -1, + PyModuleDef_HEAD_INIT, .m_name = "sd_bus_internals", .m_doc = PyDoc_STR("Sd bus internals module."), .m_methods = SdBusPyInternal_methods, .m_size = -1, }; PyObject* SdBus_class = NULL; diff --git a/src/sdbus/sd_bus_internals_bus.c b/src/sdbus/sd_bus_internals_bus.c index 8882ae2..aa6aec9 100644 --- a/src/sdbus/sd_bus_internals_bus.c +++ b/src/sdbus/sd_bus_internals_bus.c @@ -645,27 +645,24 @@ static PyObject* SdBus_start(SdBusObject* self, PyObject* Py_UNUSED(args)) { } static PyMethodDef SdBus_methods[] = { - {"call", (SD_BUS_PY_FUNC_TYPE)SdBus_call, SD_BUS_PY_METH, "Send message and get reply"}, - {"call_async", (SD_BUS_PY_FUNC_TYPE)SdBus_call_async, SD_BUS_PY_METH, "Async send message, returns awaitable future"}, - {"drive", (PyCFunction)SdBus_drive, METH_NOARGS, "Drive connection"}, - {"get_fd", (SD_BUS_PY_FUNC_TYPE)SdBus_get_fd, SD_BUS_PY_METH, "Get file descriptor to await on"}, - {"new_method_call_message", (SD_BUS_PY_FUNC_TYPE)SdBus_new_method_call_message, SD_BUS_PY_METH, NULL}, - {"new_property_get_message", (SD_BUS_PY_FUNC_TYPE)SdBus_new_property_get_message, SD_BUS_PY_METH, NULL}, - {"new_property_set_message", (SD_BUS_PY_FUNC_TYPE)SdBus_new_property_set_message, SD_BUS_PY_METH, - "Set object/interface property. User must add variant data to " - "message"}, - {"new_signal_message", (SD_BUS_PY_FUNC_TYPE)SdBus_new_signal_message, SD_BUS_PY_METH, "Create new signal message. User must data to message and send it"}, - {"add_interface", (SD_BUS_PY_FUNC_TYPE)SdBus_add_interface, SD_BUS_PY_METH, "Add interface to the bus"}, + {"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.")}, + {"drive", (PyCFunction)SdBus_drive, METH_NOARGS, PyDoc_STR("Drive connection.")}, + {"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.")}, + {"new_property_get_message", (SD_BUS_PY_FUNC_TYPE)SdBus_new_property_get_message, SD_BUS_PY_METH, PyDoc_STR("Create new empty property get message.")}, + {"new_property_set_message", (SD_BUS_PY_FUNC_TYPE)SdBus_new_property_set_message, SD_BUS_PY_METH, PyDoc_STR("Create new empty property set message.")}, + {"new_signal_message", (SD_BUS_PY_FUNC_TYPE)SdBus_new_signal_message, SD_BUS_PY_METH, PyDoc_STR("Create new empty signal message.")}, + {"add_interface", (SD_BUS_PY_FUNC_TYPE)SdBus_add_interface, SD_BUS_PY_METH, PyDoc_STR("Add interface to the bus.")}, {"get_signal_queue_async", (SD_BUS_PY_FUNC_TYPE)SdBus_get_signal_queue, SD_BUS_PY_METH, - "Returns a future that returns a queue that queues signal " - "messages"}, - {"request_name_async", (SD_BUS_PY_FUNC_TYPE)SdBus_request_name_async, SD_BUS_PY_METH, "Request D-Bus name async"}, - {"request_name", (SD_BUS_PY_FUNC_TYPE)SdBus_request_name, SD_BUS_PY_METH, "Request D-Bus name blocking"}, - {"add_object_manager", (SD_BUS_PY_FUNC_TYPE)SdBus_add_object_manager, SD_BUS_PY_METH, "Add object manager at the path"}, - {"emit_object_added", (SD_BUS_PY_FUNC_TYPE)SdBus_emit_object_added, SD_BUS_PY_METH, "Emit signal that object was added"}, - {"emit_object_removed", (SD_BUS_PY_FUNC_TYPE)SdBus_emit_object_removed, SD_BUS_PY_METH, "Emit signal that object was removed"}, - {"close", (PyCFunction)SdBus_close, METH_NOARGS, "Close connection"}, - {"start", (PyCFunction)SdBus_start, METH_NOARGS, "Start connection"}, + PyDoc_STR("Returns a future that returns a queue that queues signal messages.")}, + {"request_name_async", (SD_BUS_PY_FUNC_TYPE)SdBus_request_name_async, SD_BUS_PY_METH, PyDoc_STR("Request D-Bus name async.")}, + {"request_name", (SD_BUS_PY_FUNC_TYPE)SdBus_request_name, SD_BUS_PY_METH, PyDoc_STR("Request D-Bus name blocking.")}, + {"add_object_manager", (SD_BUS_PY_FUNC_TYPE)SdBus_add_object_manager, SD_BUS_PY_METH, PyDoc_STR("Add object manager at the path.")}, + {"emit_object_added", (SD_BUS_PY_FUNC_TYPE)SdBus_emit_object_added, SD_BUS_PY_METH, PyDoc_STR("Emit signal that object was added.")}, + {"emit_object_removed", (SD_BUS_PY_FUNC_TYPE)SdBus_emit_object_removed, SD_BUS_PY_METH, PyDoc_STR("Emit signal that object was removed.")}, + {"close", (PyCFunction)SdBus_close, METH_NOARGS, PyDoc_STR("Close connection.")}, + {"start", (PyCFunction)SdBus_start, METH_NOARGS, PyDoc_STR("Start connection.")}, {NULL, NULL, 0, NULL}, }; @@ -682,7 +679,7 @@ static PyObject* SdBus_address_getter(SdBusObject* self, void* Py_UNUSED(closure } static PyGetSetDef SdBus_properies[] = { - {"address", (getter)SdBus_address_getter, NULL, "Bus address", NULL}, + {"address", (getter)SdBus_address_getter, NULL, PyDoc_STR("Bus address."), NULL}, {0}, }; diff --git a/src/sdbus/sd_bus_internals_funcs.c b/src/sdbus/sd_bus_internals_funcs.c index 0dbee31..676d53e 100644 --- a/src/sdbus/sd_bus_internals_funcs.c +++ b/src/sdbus/sd_bus_internals_funcs.c @@ -274,21 +274,21 @@ static PyObject* is_object_path_valid(PyObject* Py_UNUSED(self), PyObject* args) } PyMethodDef SdBusPyInternal_methods[] = { - {"sd_bus_open", (PyCFunction)sd_bus_py_open, METH_NOARGS, - "Open dbus connection. Session bus running as user or system bus as " - "daemon"}, - {"sd_bus_open_user", (PyCFunction)sd_bus_py_open_user, METH_NOARGS, "Open user session dbus"}, - {"sd_bus_open_system", (PyCFunction)sd_bus_py_open_system, METH_NOARGS, "Open system dbus"}, - {"sd_bus_open_system_remote", (PyCFunction)sd_bus_py_open_system_remote, METH_VARARGS, "Open remote system bus over SSH"}, - {"sd_bus_open_user_machine", (PyCFunction)sd_bus_py_open_user_machine, METH_VARARGS, "Open system bus in systemd-nspawn container"}, - {"sd_bus_open_system_machine", (PyCFunction)sd_bus_py_open_system_machine, METH_VARARGS, "Open user bus in systemd-nspawn container"}, - {"encode_object_path", (SD_BUS_PY_FUNC_TYPE)encode_object_path, SD_BUS_PY_METH, "Encode object path with object path prefix and arbitrary string"}, - {"decode_object_path", (SD_BUS_PY_FUNC_TYPE)decode_object_path, SD_BUS_PY_METH, "Decode object path with object path prefix and arbitrary string"}, - {"map_exception_to_dbus_error", (SD_BUS_PY_FUNC_TYPE)map_exception_to_dbus_error, SD_BUS_PY_METH, "Map exception to a D-Bus error name"}, - {"add_exception_mapping", (SD_BUS_PY_FUNC_TYPE)add_exception_mapping, SD_BUS_PY_METH, "Add exception to the mapping of dbus error names"}, - {"is_interface_name_valid", (SD_BUS_PY_FUNC_TYPE)is_interface_name_valid, SD_BUS_PY_METH, "Is the string valid interface name?"}, - {"is_service_name_valid", (SD_BUS_PY_FUNC_TYPE)is_service_name_valid, SD_BUS_PY_METH, "Is the string valid service name?"}, - {"is_member_name_valid", (SD_BUS_PY_FUNC_TYPE)is_member_name_valid, SD_BUS_PY_METH, "Is the string valid member name?"}, - {"is_object_path_valid", (SD_BUS_PY_FUNC_TYPE)is_object_path_valid, SD_BUS_PY_METH, "Is the string valid object path?"}, + {"sd_bus_open", (PyCFunction)sd_bus_py_open, METH_NOARGS, PyDoc_STR("Open dbus connection. Session bus running as user or system bus as daemon.")}, + {"sd_bus_open_user", (PyCFunction)sd_bus_py_open_user, METH_NOARGS, PyDoc_STR("Open user session dbus.")}, + {"sd_bus_open_system", (PyCFunction)sd_bus_py_open_system, METH_NOARGS, PyDoc_STR("Open system dbus.")}, + {"sd_bus_open_system_remote", (PyCFunction)sd_bus_py_open_system_remote, METH_VARARGS, PyDoc_STR("Open remote system bus over SSH.")}, + {"sd_bus_open_user_machine", (PyCFunction)sd_bus_py_open_user_machine, METH_VARARGS, PyDoc_STR("Open system bus in systemd-nspawn container.")}, + {"sd_bus_open_system_machine", (PyCFunction)sd_bus_py_open_system_machine, METH_VARARGS, PyDoc_STR("Open user bus in systemd-nspawn container.")}, + {"encode_object_path", (SD_BUS_PY_FUNC_TYPE)encode_object_path, SD_BUS_PY_METH, + PyDoc_STR("Encode object path with object path prefix and arbitrary string.")}, + {"decode_object_path", (SD_BUS_PY_FUNC_TYPE)decode_object_path, SD_BUS_PY_METH, + PyDoc_STR("Decode object path with object path prefix and arbitrary string.")}, + {"map_exception_to_dbus_error", (SD_BUS_PY_FUNC_TYPE)map_exception_to_dbus_error, SD_BUS_PY_METH, PyDoc_STR("Map exception to a D-Bus error name.")}, + {"add_exception_mapping", (SD_BUS_PY_FUNC_TYPE)add_exception_mapping, SD_BUS_PY_METH, PyDoc_STR("Add exception to the mapping of dbus error names.")}, + {"is_interface_name_valid", (SD_BUS_PY_FUNC_TYPE)is_interface_name_valid, SD_BUS_PY_METH, PyDoc_STR("Is the string valid interface name?")}, + {"is_service_name_valid", (SD_BUS_PY_FUNC_TYPE)is_service_name_valid, SD_BUS_PY_METH, PyDoc_STR("Is the string valid service name?")}, + {"is_member_name_valid", (SD_BUS_PY_FUNC_TYPE)is_member_name_valid, SD_BUS_PY_METH, PyDoc_STR("Is the string valid member name?")}, + {"is_object_path_valid", (SD_BUS_PY_FUNC_TYPE)is_object_path_valid, SD_BUS_PY_METH, PyDoc_STR("Is the string valid object path?")}, {NULL, NULL, 0, NULL}, }; diff --git a/src/sdbus/sd_bus_internals_interface.c b/src/sdbus/sd_bus_internals_interface.c index 5e518f1..2b9b038 100644 --- a/src/sdbus/sd_bus_internals_interface.c +++ b/src/sdbus/sd_bus_internals_interface.c @@ -321,10 +321,10 @@ static PyObject* SdBusInterface_create_vtable(SdBusInterfaceObject* self, PyObje } static PyMethodDef SdBusInterface_methods[] = { - {"add_method", (SD_BUS_PY_FUNC_TYPE)SdBusInterface_add_method, SD_BUS_PY_METH, "Add method to the dbus interface"}, - {"add_property", (SD_BUS_PY_FUNC_TYPE)SdBusInterface_add_property, SD_BUS_PY_METH, "Add property to the dbus interface"}, - {"add_signal", (SD_BUS_PY_FUNC_TYPE)SdBusInterface_add_signal, SD_BUS_PY_METH, "Add signal to the dbus interface"}, - {"_create_vtable", (PyCFunction)SdBusInterface_create_vtable, METH_NOARGS, "Creates the vtable"}, + {"add_method", (SD_BUS_PY_FUNC_TYPE)SdBusInterface_add_method, SD_BUS_PY_METH, PyDoc_STR("Add method to the D-Bus interface.")}, + {"add_property", (SD_BUS_PY_FUNC_TYPE)SdBusInterface_add_property, SD_BUS_PY_METH, PyDoc_STR("Add property to the D-Bus interface.")}, + {"add_signal", (SD_BUS_PY_FUNC_TYPE)SdBusInterface_add_signal, SD_BUS_PY_METH, PyDoc_STR("Add signal to the D-Bus interface.")}, + {"_create_vtable", (PyCFunction)SdBusInterface_create_vtable, METH_NOARGS, PyDoc_STR("Creates the vtable.")}, {NULL, NULL, 0, NULL}, }; diff --git a/src/sdbus/sd_bus_internals_message.c b/src/sdbus/sd_bus_internals_message.c index f6e4fb9..ea9a9b8 100644 --- a/src/sdbus/sd_bus_internals_message.c +++ b/src/sdbus/sd_bus_internals_message.c @@ -1003,17 +1003,18 @@ static SdBusMessageObject* SdBusMessage_create_error_reply(SdBusMessageObject* s } static PyMethodDef SdBusMessage_methods[] = { - {"append_data", (SD_BUS_PY_FUNC_TYPE)SdBusMessage_append_data, SD_BUS_PY_METH, "Append basic data based on signature."}, - {"open_container", (SD_BUS_PY_FUNC_TYPE)SdBusMessage_open_container, SD_BUS_PY_METH, "Open container for writing"}, - {"close_container", (PyCFunction)SdBusMessage_close_container, METH_NOARGS, "Close container"}, - {"enter_container", (SD_BUS_PY_FUNC_TYPE)SdBusMessage_enter_container, SD_BUS_PY_METH, "Enter container for reading"}, - {"exit_container", (PyCFunction)SdBusMessage_exit_container, METH_NOARGS, "Exit container"}, - {"dump", (PyCFunction)SdBusMessage_dump, METH_NOARGS, "Dump message to stdout"}, - {"seal", (PyCFunction)SdBusMessage_seal, METH_NOARGS, "Seal message contents"}, - {"get_contents", (PyCFunction)SdBusMessage_get_contents2, METH_NOARGS, "Iterate over message contents"}, - {"create_reply", (PyCFunction)SdBusMessage_create_reply, METH_NOARGS, "Create reply message"}, - {"create_error_reply", (SD_BUS_PY_FUNC_TYPE)SdBusMessage_create_error_reply, SD_BUS_PY_METH, "Create error reply with error name and error message"}, - {"send", (PyCFunction)SdBusMessage_send, METH_NOARGS, "Queue message to be sent"}, + {"append_data", (SD_BUS_PY_FUNC_TYPE)SdBusMessage_append_data, SD_BUS_PY_METH, PyDoc_STR("Append basic data based on signature.")}, + {"open_container", (SD_BUS_PY_FUNC_TYPE)SdBusMessage_open_container, SD_BUS_PY_METH, PyDoc_STR("Open container for writing.")}, + {"close_container", (PyCFunction)SdBusMessage_close_container, METH_NOARGS, PyDoc_STR("Close container.")}, + {"enter_container", (SD_BUS_PY_FUNC_TYPE)SdBusMessage_enter_container, SD_BUS_PY_METH, PyDoc_STR("Enter container for reading.")}, + {"exit_container", (PyCFunction)SdBusMessage_exit_container, METH_NOARGS, PyDoc_STR("Exit container.")}, + {"dump", (PyCFunction)SdBusMessage_dump, METH_NOARGS, PyDoc_STR("Dump message to stdout.")}, + {"seal", (PyCFunction)SdBusMessage_seal, METH_NOARGS, PyDoc_STR("Seal message contents.")}, + {"get_contents", (PyCFunction)SdBusMessage_get_contents2, METH_NOARGS, PyDoc_STR("Iterate over message contents.")}, + {"create_reply", (PyCFunction)SdBusMessage_create_reply, METH_NOARGS, PyDoc_STR("Create reply message.")}, + {"create_error_reply", (SD_BUS_PY_FUNC_TYPE)SdBusMessage_create_error_reply, SD_BUS_PY_METH, + PyDoc_STR("Create error reply with error name and error message.")}, + {"send", (PyCFunction)SdBusMessage_send, METH_NOARGS, PyDoc_STR("Queue message to be sent.")}, {NULL, NULL, 0, NULL}, }; @@ -1083,12 +1084,12 @@ static PyObject* SdBusMessage_sender_getter(SdBusMessageObject* self, void* Py_U } static PyGetSetDef SdBusMessage_properies[] = { - {"expect_reply", (getter)SdBusMessage_expect_reply_getter, (setter)SdBusMessage_expect_reply_setter, "Expect reply message?", NULL}, - {"destination", (getter)SdBusMessage_destination_getter, NULL, "Message destination service name", NULL}, - {"path", (getter)SdBusMessage_path_getter, NULL, "Message destination object path", NULL}, - {"interface", (getter)SdBusMessage_interface_getter, NULL, "Message destination interface name", NULL}, - {"member", (getter)SdBusMessage_member_getter, NULL, "Message destination member name", NULL}, - {"sender", (getter)SdBusMessage_sender_getter, NULL, "Message sender name", NULL}, + {"expect_reply", (getter)SdBusMessage_expect_reply_getter, (setter)SdBusMessage_expect_reply_setter, PyDoc_STR("Expect reply message?"), NULL}, + {"destination", (getter)SdBusMessage_destination_getter, NULL, PyDoc_STR("Message destination service name."), NULL}, + {"path", (getter)SdBusMessage_path_getter, NULL, PyDoc_STR("Message destination object path."), NULL}, + {"interface", (getter)SdBusMessage_interface_getter, NULL, PyDoc_STR("Message destination interface name."), NULL}, + {"member", (getter)SdBusMessage_member_getter, NULL, PyDoc_STR("Message destination member name."), NULL}, + {"sender", (getter)SdBusMessage_sender_getter, NULL, PyDoc_STR("Message sender name."), NULL}, {0}, }; From fa9e5efd6610e4fdf333715cfc097b7d40ce8e0e Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sat, 26 Aug 2023 21:50:24 +0600 Subject: [PATCH 031/188] Interface generator - add support for "write" type properties Those properties will have the setter code generated but have difference compared to usual "readwrite" properties. sd-bus does not look like supporting "write" properties anyway. Also fix trying to access the `is_read_only` attribute on unknown property access type. Use actual `access_type` variable to raise exception. --- src/sdbus/interface_generator.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sdbus/interface_generator.py b/src/sdbus/interface_generator.py index 402439a..8f53dbe 100644 --- a/src/sdbus/interface_generator.py +++ b/src/sdbus/interface_generator.py @@ -436,12 +436,12 @@ def __init__(self, element: Element): self.is_explicit = False access_type = element.attrib['access'] - if access_type == 'readwrite': + if access_type == 'readwrite' or access_type == 'write': self.is_read_only = False elif access_type == 'read': self.is_read_only = True else: - raise ValueError(f"Unknown property access {self.is_read_only}") + raise ValueError(f"Unknown property access {access_type}") super().__init__(element) From 23bc01c64f594992c7ba64a3a5cbf23453116592 Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sun, 27 Aug 2023 14:26:35 +0600 Subject: [PATCH 032/188] Fix readthedocs documentation build Explicitly set the `html_theme` variable as readthedocs append config that uses that variable. Also update the `.readthedocs.yaml` to more recent standard and bump Python version to 3.9 --- .readthedocs.yaml | 9 +++++++-- docs/conf.py | 1 + 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 2112b9c..92cdff1 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -2,5 +2,10 @@ version: 2 -python: - version: 3.8 +build: + os: "ubuntu-22.04" + tools: + python: "3.9" + +sphinx: + configuration: "docs/conf.py" diff --git a/docs/conf.py b/docs/conf.py index ab64486..f61f4f9 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -25,6 +25,7 @@ author = 'igo95862' source_suffix = '.rst' extensions = ['sdbus.autodoc'] +html_theme = "sphinx_rtd_theme" autoclass_content = 'both' autodoc_typehints = 'description' From 87aba22c22fa871fda2fc52021eade3c8055fe73 Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sun, 8 Oct 2023 23:06:41 +0600 Subject: [PATCH 033/188] Remove unused for loop in DbusPropertiesInterfaceAsync It was looping over all members but did not actualy do anything since 624e6115c9bb7eaceb83c844c1be3391be5f8889 . --- src/sdbus/dbus_proxy_async_interfaces.py | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/src/sdbus/dbus_proxy_async_interfaces.py b/src/sdbus/dbus_proxy_async_interfaces.py index 312b446..3f21171 100644 --- a/src/sdbus/dbus_proxy_async_interfaces.py +++ b/src/sdbus/dbus_proxy_async_interfaces.py @@ -19,15 +19,13 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA from __future__ import annotations -from inspect import getmembers from typing import Any, Dict, List, Literal, Optional, Tuple from .dbus_common_funcs import _parse_properties_vardict, get_default_bus from .dbus_proxy_async_interface_base import DbusInterfaceBaseAsync from .dbus_proxy_async_method import dbus_method_async -from .dbus_proxy_async_property import DbusPropertyAsyncBinded from .dbus_proxy_async_signal import dbus_signal_async -from .sd_bus_internals import DbusPropertyEmitsChangeFlag, SdBus, SdBusSlot +from .sd_bus_internals import SdBus, SdBusSlot class DbusPeerInterfaceAsync( @@ -66,13 +64,6 @@ class DbusPropertiesInterfaceAsync( interface_name='org.freedesktop.DBus.Properties', serving_enabled=False, ): - def __init__(self) -> None: - super().__init__() - - for key, value in getmembers(self): - if isinstance(value, DbusPropertyAsyncBinded): - if not value.dbus_property.flags & DbusPropertyEmitsChangeFlag: - continue @dbus_signal_async('sa{sv}as') def properties_changed(self) -> DBUS_PROPERTIES_CHANGED_TYPING: From c1d9f2f52e2ab4be5e13c295bc36febddf44c476 Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sun, 15 Oct 2023 23:04:44 +0600 Subject: [PATCH 034/188] readthedocs: Install sphinx_rtd_theme The sphinx_rtd_theme is no longer installed by default. --- .readthedocs.yaml | 4 ++++ docs/requirements.txt | 3 +++ 2 files changed, 7 insertions(+) create mode 100644 docs/requirements.txt diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 92cdff1..2ad865e 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -9,3 +9,7 @@ build: sphinx: configuration: "docs/conf.py" + +python: + install: + - requirements: docs/requirements.txt diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 0000000..88760ba --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,3 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# Copyright (C) 2023 igo95862 +sphinx_rtd_theme From 223c36cde530813347e1d6fcb8b19c2d74816278 Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sat, 28 Oct 2023 14:40:07 +0600 Subject: [PATCH 035/188] Rewind message before parsing it in `SdBusMessage.get_contents` The same message can be given to multiple handlers for example in case of multiple tasks awaiting on signals. This would cause the "Dbus type '\x00' is unknown" errors. --- src/sdbus/sd_bus_internals_message.c | 3 ++- test/test_read_write_dbus_types.py | 8 ++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/sdbus/sd_bus_internals_message.c b/src/sdbus/sd_bus_internals_message.c index ea9a9b8..588f107 100644 --- a/src/sdbus/sd_bus_internals_message.c +++ b/src/sdbus/sd_bus_internals_message.c @@ -955,7 +955,7 @@ static PyObject* iter_tuple_or_single(_Parse_state* parser) { } static PyObject* SdBusMessage_get_contents2(SdBusMessageObject* self, PyObject* Py_UNUSED(args)) { - const char* message_signature = sd_bus_message_get_signature(self->message_ref, 0); + const char* message_signature = sd_bus_message_get_signature(self->message_ref, 1); if (message_signature == NULL) { PyErr_SetString(PyExc_TypeError, "Failed to get message signature."); @@ -966,6 +966,7 @@ static PyObject* SdBusMessage_get_contents2(SdBusMessageObject* self, PyObject* Py_RETURN_NONE; } + CALL_SD_BUS_AND_CHECK(sd_bus_message_rewind(self->message_ref, 1)); _Parse_state read_parser = { .message = self->message_ref, .container_char_ptr = message_signature, diff --git a/test/test_read_write_dbus_types.py b/test/test_read_write_dbus_types.py index d3a1a01..0fe7170 100644 --- a/test/test_read_write_dbus_types.py +++ b/test/test_read_write_dbus_types.py @@ -429,6 +429,14 @@ class TestEnum(str, Enum): self.assertEqual(message.get_contents(), TestEnum.SOMETHING) + def test_reading_multiple_times(self) -> None: + message = create_message(self.bus) + message.append_data('s', 'test') + message.seal() + + for _ in range(5): + message.get_contents() + if __name__ == "__main__": main() From 65b257da52ed377057f87d359c75014295e626b8 Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sat, 28 Oct 2023 15:54:15 +0600 Subject: [PATCH 036/188] Only rewind the current container in `SdBusMessage.get_contents` Otherwise properties break because the actual property set message consists of tuple of interface and property name and variant of value. The libsystemd will place the cursor to only read the value from message but completely rewinding message will point again to the tuple. --- src/sdbus/sd_bus_internals_message.c | 4 ++-- test/test_read_write_dbus_types.py | 5 ++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/sdbus/sd_bus_internals_message.c b/src/sdbus/sd_bus_internals_message.c index 588f107..85758db 100644 --- a/src/sdbus/sd_bus_internals_message.c +++ b/src/sdbus/sd_bus_internals_message.c @@ -955,7 +955,7 @@ static PyObject* iter_tuple_or_single(_Parse_state* parser) { } static PyObject* SdBusMessage_get_contents2(SdBusMessageObject* self, PyObject* Py_UNUSED(args)) { - const char* message_signature = sd_bus_message_get_signature(self->message_ref, 1); + const char* message_signature = sd_bus_message_get_signature(self->message_ref, 0); if (message_signature == NULL) { PyErr_SetString(PyExc_TypeError, "Failed to get message signature."); @@ -966,7 +966,7 @@ static PyObject* SdBusMessage_get_contents2(SdBusMessageObject* self, PyObject* Py_RETURN_NONE; } - CALL_SD_BUS_AND_CHECK(sd_bus_message_rewind(self->message_ref, 1)); + CALL_SD_BUS_AND_CHECK(sd_bus_message_rewind(self->message_ref, 0)); _Parse_state read_parser = { .message = self->message_ref, .container_char_ptr = message_signature, diff --git a/test/test_read_write_dbus_types.py b/test/test_read_write_dbus_types.py index 0fe7170..3f7ccb7 100644 --- a/test/test_read_write_dbus_types.py +++ b/test/test_read_write_dbus_types.py @@ -435,7 +435,10 @@ def test_reading_multiple_times(self) -> None: message.seal() for _ in range(5): - message.get_contents() + self.assertEqual( + message.get_contents(), + "test", + ) if __name__ == "__main__": From dda82245e6c261378d057ba35f84f1ddd625e465 Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sat, 28 Oct 2023 16:15:40 +0600 Subject: [PATCH 037/188] test: Test signal with multiple readers This checks that messages are properly rewinded between readers. --- test/test_sd_bus_async.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/test/test_sd_bus_async.py b/test/test_sd_bus_async.py index 6c205ec..554c77c 100644 --- a/test/test_sd_bus_async.py +++ b/test/test_sd_bus_async.py @@ -518,6 +518,33 @@ async def catch_anywhere_oneshot_local( timeout=1, ) + async def test_signal_multiple_readers(self) -> None: + test_object, test_object_connection = initialize_object() + + loop = get_running_loop() + + test_tuple = ('sgfsretg', 'asd') + + async def reader_one() -> Tuple[str, str]: + async for x in test_object_connection.test_signal.catch(): + return test_tuple + + raise RuntimeError + + async def reader_two() -> Tuple[str, str]: + async for x in test_object_connection.test_signal.catch(): + return test_tuple + + raise RuntimeError + + t1 = loop.create_task(reader_one()) + t2 = loop.create_task(reader_two()) + + loop.call_at(0, test_object.test_signal.emit, test_tuple) + + self.assertEqual(test_tuple, await wait_for(t1, timeout=1)) + self.assertEqual(test_tuple, await wait_for(t2, timeout=1)) + async def test_exceptions(self) -> None: test_object, test_object_connection = initialize_object() From 92c0427de2bb508f08481ff5ec2ee99c2cc4b77c Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sat, 28 Oct 2023 21:06:30 +0600 Subject: [PATCH 038/188] Rework interface generator to use template imports This will make it easier to implement the blocking generator. Also use double qoutes instead of single qoutes. --- src/sdbus/interface_generator.py | 193 ++++++++++++++++++------------- 1 file changed, 111 insertions(+), 82 deletions(-) diff --git a/src/sdbus/interface_generator.py b/src/sdbus/interface_generator.py index 8f53dbe..59b26fc 100644 --- a/src/sdbus/interface_generator.py +++ b/src/sdbus/interface_generator.py @@ -540,89 +540,117 @@ def __init__(self, element: Element): else: raise ValueError(f'Unknown D-Bus member {dbus_member}') - def generate_interface_class(self) -> str: - from jinja2 import Environment as JinjaEnv - env = JinjaEnv(trim_blocks=True) - template = env.from_string(async_interface_template_txt) - - return template.render(interface=self) - - -async_import_header_txt = """ -from __future__ import annotations +SKIP_INTERFACES = { + 'org.freedesktop.DBus.Properties', + 'org.freedesktop.DBus.Introspectable', + 'org.freedesktop.DBus.Peer', + 'org.freedesktop.DBus.ObjectManager', +} -from typing import Any, Dict, List, Tuple +INTERFACE_TEMPLATES: Dict[str, str] = { + "generic_header": r"""from __future__ import annotations -from sdbus import (DbusDeprecatedFlag, DbusInterfaceCommonAsync, - DbusNoReplyFlag, DbusPropertyConstFlag, - DbusPropertyEmitsChangeFlag, - DbusPropertyEmitsInvalidationFlag, DbusPropertyExplicitFlag, - DbusUnprivilegedFlag, dbus_method_async, - dbus_property_async, dbus_signal_async) +from typing import Any, Dict, List, Tuple""", + "async_imports_header": r"""from sdbus import ( + DbusDeprecatedFlag, + DbusInterfaceCommonAsync, + DbusNoReplyFlag, + DbusPropertyConstFlag, + DbusPropertyEmitsChangeFlag, + DbusPropertyEmitsInvalidationFlag, + DbusPropertyExplicitFlag, + DbusUnprivilegedFlag, + dbus_method_async, + dbus_property_async, + dbus_signal_async, +)""", + "async_main": ( + r"""{% if include_import_header -%} +{% include 'generic_header' %} + +{% include 'async_imports_header' %} +{%- endif %} +{% for interface in interfaces %} + +{% include 'async_interface' %} +{%- endfor %} """ - -async_interface_template_txt = """ - -class {{ interface.python_name }}( + ), + "async_interface": ( + r"""class {{ interface.python_name }}( DbusInterfaceCommonAsync, - interface_name='{{ interface.interface_name }}', + interface_name="{{ interface.interface_name }}", ): -{% for method in interface.methods %} - - @dbus_method_async( -{% if method.dbus_input_signature %} - input_signature='{{ method.dbus_input_signature }}', -{% endif %} -{% if method.dbus_result_signature %} - result_signature='{{ method.dbus_result_signature }}', -{% endif %} -{% if method.flags_str %} - flags={{ method.flags_str }}, -{% endif %} - ) - async def {{ method.python_name }}( - self, -{% for arg_name, arg_type in method.args_names_and_typing %} - {{ arg_name }}: {{ arg_type }}, -{% endfor %} - ) -> {{ method.result_typing }}: - raise NotImplementedError -{% endfor %} -{% for a_property in interface.properties %} - - @dbus_property_async( -{% if a_property.dbus_signature %} - property_signature='{{ a_property.dbus_signature }}', -{% endif %} -{% if a_property.flags_str %} - flags={{ a_property.flags_str }}, -{% endif %} - ) - def {{ a_property.python_name }}(self) -> {{ a_property.typing }}: - raise NotImplementedError -{% endfor %} -{% for signal in interface.signals %} - - @dbus_signal_async( -{% if signal.dbus_signature %} - signal_signature='{{ signal.dbus_signature }}', -{% endif %} -{% if signal.flags_str %} - flags={{ signal.flags_str }}, -{% endif %} - ) - def {{ signal.python_name }}(self) -> {{ signal.typing }}: - raise NotImplementedError -{% endfor %} - +{%- filter indent -%} +{% for method in interface.methods -%} +{% include 'async_method' %} +{% endfor -%} +{% for a_property in interface.properties -%} +{% include 'async_property' %} +{% endfor -%} +{% for signal in interface.signals -%} +{% include 'async_signal' %} +{% endfor -%} +{%- endfilter -%} """ - -SKIP_INTERFACES = { - 'org.freedesktop.DBus.Properties', - 'org.freedesktop.DBus.Introspectable', - 'org.freedesktop.DBus.Peer', - 'org.freedesktop.DBus.ObjectManager', + ), + "async_method": ( + r""" +@dbus_method_async( + +{%- if method.dbus_input_signature %} + input_signature="{{ method.dbus_input_signature }}", +{%- endif %} + +{%- if method.dbus_result_signature %} + result_signature="{{ method.dbus_result_signature }}", +{%- endif %} + +{%- if method.flags_str %} + flags={{ method.flags_str }}, +{%- endif %} +) +async def {{ method.python_name }}( + self, + +{%- for arg_name, arg_type in method.args_names_and_typing %} + {{ arg_name }}: {{ arg_type }}, +{%- endfor %} +) -> {{ method.result_typing }}: + raise NotImplementedError +""" + ), + "async_property": ( + r""" +@dbus_property_async( + +{%- if a_property.dbus_signature %} + property_signature="{{ a_property.dbus_signature }}", +{%- endif %} + +{%- if a_property.flags_str %} + flags={{ a_property.flags_str }}, +{%- endif %} +) +def {{ a_property.python_name }}(self) -> {{ a_property.typing }}: + raise NotImplementedError""" + ), + "async_signal": ( + r""" +@dbus_signal_async( + +{%- if signal.dbus_signature %} + signal_signature="{{ signal.dbus_signature }}", +{%- endif %} + +{%- if signal.flags_str %} + flags={{ signal.flags_str }}, +{%- endif %} +) +def {{ signal.python_name }}(self) -> {{ signal.typing }}: + raise NotImplementedError""" + ), } @@ -666,10 +694,11 @@ def generate_async_py_file( interfaces: List[DbusInterfaceIntrospection], include_import_header: bool = True) -> str: - interfaces_definitions = '\n'.join( - (x.generate_interface_class() for x in interfaces)) + from jinja2 import DictLoader + from jinja2 import Environment as JinjaEnv - if include_import_header: - return async_import_header_txt + interfaces_definitions - else: - return interfaces_definitions + env = JinjaEnv(loader=DictLoader(INTERFACE_TEMPLATES)) + return env.get_template("async_main").render( + interfaces=interfaces, + include_import_header=include_import_header, + ) From 46d72a626da0f45e314f29015d7c83ea513484ab Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sat, 28 Oct 2023 21:23:31 +0600 Subject: [PATCH 039/188] Use stdout instead of print to output generated interfaces `print` adds an extra new line. --- src/sdbus/__main__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/sdbus/__main__.py b/src/sdbus/__main__.py index 7081468..a600d04 100644 --- a/src/sdbus/__main__.py +++ b/src/sdbus/__main__.py @@ -21,6 +21,7 @@ from argparse import ArgumentParser, Namespace from pathlib import Path +from sys import stdout from typing import List from .interface_generator import ( @@ -48,7 +49,7 @@ def run_gen_from_connection(namespace: Namespace) -> None: itrospection = connection.dbus_introspect() interfaces.extend(interfaces_from_str(itrospection)) - print( + stdout.write( generate_async_py_file( interfaces, namespace.no_imports_header)) @@ -59,7 +60,7 @@ def run_gen_from_file(namespace: Namespace) -> None: for file in namespace.filenames: interfaces.extend(interfaces_from_file(file)) - print( + stdout.write( generate_async_py_file( interfaces, namespace.no_imports_header)) From bd2a2594a695fd8e9cd1d34f01d307a7217e54d9 Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sat, 28 Oct 2023 22:17:32 +0600 Subject: [PATCH 040/188] Add `--imports-header` option to interface generator It is the opposite to `-no-imports-header` option. The last option takes the priority. --- src/sdbus/__main__.py | 60 ++++++++++++++++++++++++++----------------- 1 file changed, 36 insertions(+), 24 deletions(-) diff --git a/src/sdbus/__main__.py b/src/sdbus/__main__.py index a600d04..65d745e 100644 --- a/src/sdbus/__main__.py +++ b/src/sdbus/__main__.py @@ -19,10 +19,10 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA from __future__ import annotations -from argparse import ArgumentParser, Namespace +from argparse import ArgumentParser from pathlib import Path from sys import stdout -from typing import List +from typing import List, Optional from .interface_generator import ( DbusInterfaceIntrospection, @@ -32,13 +32,18 @@ ) -def run_gen_from_connection(namespace: Namespace) -> None: - connection_name = namespace.connection_name - object_paths = namespace.object_paths +def run_gen_from_connection( + connection_name: str, + object_paths: List[str], + system: bool, + imports_header: bool, +) -> None: + connection_name = connection_name + object_paths = object_paths from .dbus_proxy_sync_interfaces import DbusInterfaceCommon - if namespace.system: + if system: from .dbus_common_funcs import set_default_bus from .sd_bus_internals import sd_bus_open_system set_default_bus(sd_bus_open_system()) @@ -51,21 +56,21 @@ def run_gen_from_connection(namespace: Namespace) -> None: stdout.write( generate_async_py_file( - interfaces, namespace.no_imports_header)) + interfaces, imports_header)) -def run_gen_from_file(namespace: Namespace) -> None: +def run_gen_from_file(filenames: List[str], imports_header: bool) -> None: interfaces: List[DbusInterfaceIntrospection] = [] - for file in namespace.filenames: + for file in filenames: interfaces.extend(interfaces_from_file(file)) stdout.write( generate_async_py_file( - interfaces, namespace.no_imports_header)) + interfaces, imports_header)) -def generator_main() -> None: +def generator_main(args: Optional[List[str]] = None) -> None: main_arg_parser = ArgumentParser() subparsers = main_arg_parser.add_subparsers() @@ -73,17 +78,27 @@ def generator_main() -> None: generate_from_file_parser = subparsers.add_parser('gen-from-file') generate_from_file_parser.set_defaults(func=run_gen_from_file) - generate_from_file_parser.add_argument( - 'filenames', type=Path, nargs='+') + generate_from_connection = subparsers.add_parser('gen-from-connection') + generate_from_connection.set_defaults(func=run_gen_from_connection) + + # Common options + for subparser in (generate_from_file_parser, generate_from_connection): + subparser.add_argument( + "--no-imports-header", action="store_false", + dest="imports_header", + help="Do NOT include 'import' header", + ) + subparser.add_argument( + "--imports-header", action="store_true", default=True, + dest="imports_header", + help="Include 'import' header (default)", + ) generate_from_file_parser.add_argument( - '--no-imports-header', action='store_false', default=True, - help="Do NOT include 'import' header", + 'filenames', type=Path, nargs='+', + help="Paths to interface XML introspection files" ) - generate_from_connection = subparsers.add_parser('gen-from-connection') - generate_from_connection.set_defaults(func=run_gen_from_connection) - generate_from_connection.add_argument( 'connection_name', help=( @@ -99,18 +114,15 @@ def generator_main() -> None: 'One or more.' ) ) - generate_from_connection.add_argument( - '--no-imports-header', action='store_false', default=True, - help="Do NOT include 'import' header", - ) generate_from_connection.add_argument( '--system', help='Use system D-Bus instead of session.', action='store_true', ) - args = main_arg_parser.parse_args() - args.func(args) + args_dict = vars(main_arg_parser.parse_args(args)) + func = args_dict.pop("func") + func(**args_dict) if __name__ == "__main__": From d60ba5f4c85c327b017301df8e3fa380e83cf849 Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sat, 28 Oct 2023 22:20:39 +0600 Subject: [PATCH 041/188] test: Add test for interface generator from D-Bus connection --- test/test_interface_generator.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/test/test_interface_generator.py b/test/test_interface_generator.py index 51ef607..9b17f48 100644 --- a/test/test_interface_generator.py +++ b/test/test_interface_generator.py @@ -21,7 +21,9 @@ from importlib.util import find_spec from unittest import SkipTest, TestCase, main +from unittest.mock import MagicMock, patch +from sdbus.__main__ import generator_main from sdbus.interface_generator import ( DbusSigToTyping, camel_case_to_snake_case, @@ -29,6 +31,7 @@ interface_name_to_class, interfaces_from_str, ) +from sdbus.unittest import IsolatedDbusTestCase test_xml = """ None: self.assertIn('flags=DbusPropertyConstFlag', generated) +class TestGeneratorAgainstDbus(IsolatedDbusTestCase): + def test_generate_from_connection(self) -> None: + if find_spec('jinja2') is None: + raise SkipTest('Jinja2 not installed') + + with patch("sdbus.__main__.stdout") as stdout_mock: + generator_main( + [ + "gen-from-connection", + "org.freedesktop.DBus", + "/org/freedesktop/DBus", + ] + ) + + write_mock: MagicMock = stdout_mock.write + write_mock.assert_called_once() + + generated_interface = write_mock.call_args.args[0] + + self.assertIn( + "OrgFreedesktopDBusDebugStatsInterface", + generated_interface, + ) + self.assertIn( + "get_connection_unix_process_id", + generated_interface, + ) + + if __name__ == "__main__": main() From 8af9ea305abfd0dc747a6dd78974c46266a712a0 Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sat, 28 Oct 2023 23:36:05 +0600 Subject: [PATCH 042/188] Add `--block` option to interface generators Generates blocking interfaces code when passed instead of async. --- docs/code_generator.rst | 3 +- src/sdbus/__main__.py | 36 +++++++-- src/sdbus/interface_generator.py | 130 +++++++++++++++++++++++++------ test/test_interface_generator.py | 36 ++++++++- 4 files changed, 172 insertions(+), 33 deletions(-) diff --git a/docs/code_generator.rst b/docs/code_generator.rst index 950a53e..7f18f2e 100644 --- a/docs/code_generator.rst +++ b/docs/code_generator.rst @@ -3,7 +3,8 @@ Interface code generator Python-sdbus is able to generate the interfaces code from the D-Bus introspection XML. (either from a file or live object on D-Bus) -Currently only async interfaces code can be generated. +Currently async interfaces code is generated by default. +Blocking interfaces can be generated by passing ``--block`` option. Running code generator requires `Jinja2 `_ diff --git a/src/sdbus/__main__.py b/src/sdbus/__main__.py index 65d745e..b0e8c61 100644 --- a/src/sdbus/__main__.py +++ b/src/sdbus/__main__.py @@ -26,7 +26,7 @@ from .interface_generator import ( DbusInterfaceIntrospection, - generate_async_py_file, + generate_py_file, interfaces_from_file, interfaces_from_str, ) @@ -37,6 +37,7 @@ def run_gen_from_connection( object_paths: List[str], system: bool, imports_header: bool, + do_async: bool, ) -> None: connection_name = connection_name object_paths = object_paths @@ -55,19 +56,31 @@ def run_gen_from_connection( interfaces.extend(interfaces_from_str(itrospection)) stdout.write( - generate_async_py_file( - interfaces, imports_header)) + generate_py_file( + interfaces, + imports_header, + do_async, + ) + ) -def run_gen_from_file(filenames: List[str], imports_header: bool) -> None: +def run_gen_from_file( + filenames: List[str], + imports_header: bool, + do_async: bool, +) -> None: interfaces: List[DbusInterfaceIntrospection] = [] for file in filenames: interfaces.extend(interfaces_from_file(file)) stdout.write( - generate_async_py_file( - interfaces, imports_header)) + generate_py_file( + interfaces, + imports_header, + do_async, + ) + ) def generator_main(args: Optional[List[str]] = None) -> None: @@ -94,6 +107,17 @@ def generator_main(args: Optional[List[str]] = None) -> None: help="Include 'import' header (default)", ) + subparser.add_argument( + "--async", action="store_true", default=True, + dest="do_async", + help="Generate async interfaces (default)", + ) + subparser.add_argument( + "--block", action="store_false", + dest="do_async", + help="Generate blocking interfaces", + ) + generate_from_file_parser.add_argument( 'filenames', type=Path, nargs='+', help="Paths to interface XML introspection files" diff --git a/src/sdbus/interface_generator.py b/src/sdbus/interface_generator.py index 59b26fc..e9aac27 100644 --- a/src/sdbus/interface_generator.py +++ b/src/sdbus/interface_generator.py @@ -549,6 +549,32 @@ def __init__(self, element: Element): } INTERFACE_TEMPLATES: Dict[str, str] = { + "generic_method_flags": ( + r""" +{%- if method.dbus_input_signature %} +input_signature="{{ method.dbus_input_signature }}", +{%- endif %} + +{%- if method.dbus_result_signature %} +result_signature="{{ method.dbus_result_signature }}", +{%- endif %} + +{%- if method.flags_str %} +flags={{ method.flags_str }}, +{%- endif %} +""" + ), + "generic_property_flags": ( + r""" +{%- if a_property.dbus_signature %} +property_signature="{{ a_property.dbus_signature }}", +{%- endif %} + +{%- if a_property.flags_str %} +flags={{ a_property.flags_str }}, +{%- endif %} +""" + ), "generic_header": r"""from __future__ import annotations from typing import Any, Dict, List, Tuple""", @@ -598,18 +624,9 @@ def __init__(self, element: Element): "async_method": ( r""" @dbus_method_async( - -{%- if method.dbus_input_signature %} - input_signature="{{ method.dbus_input_signature }}", -{%- endif %} - -{%- if method.dbus_result_signature %} - result_signature="{{ method.dbus_result_signature }}", -{%- endif %} - -{%- if method.flags_str %} - flags={{ method.flags_str }}, -{%- endif %} +{%- filter indent -%} +{%- include 'generic_method_flags' -%} +{%- endfilter %} ) async def {{ method.python_name }}( self, @@ -624,14 +641,9 @@ async def {{ method.python_name }}( "async_property": ( r""" @dbus_property_async( - -{%- if a_property.dbus_signature %} - property_signature="{{ a_property.dbus_signature }}", -{%- endif %} - -{%- if a_property.flags_str %} - flags={{ a_property.flags_str }}, -{%- endif %} +{%- filter indent -%} +{%- include 'generic_property_flags' -%} +{%- endfilter %} ) def {{ a_property.python_name }}(self) -> {{ a_property.typing }}: raise NotImplementedError""" @@ -651,6 +663,72 @@ def {{ a_property.python_name }}(self) -> {{ a_property.typing }}: def {{ signal.python_name }}(self) -> {{ signal.typing }}: raise NotImplementedError""" ), + "blocking_imports_header": r"""from sdbus import ( + DbusDeprecatedFlag, + DbusInterfaceCommon, + DbusNoReplyFlag, + DbusPropertyConstFlag, + DbusPropertyEmitsChangeFlag, + DbusPropertyEmitsInvalidationFlag, + DbusPropertyExplicitFlag, + DbusUnprivilegedFlag, + dbus_method, + dbus_property, +)""", + "blocking_main": ( + r"""{% if include_import_header -%} +{% include 'generic_header' %} + +{% include 'blocking_imports_header' %} +{%- endif %} +{% for interface in interfaces %} + +{% include 'blocking_interface' %} +{%- endfor %} +""" + ), + "blocking_interface": ( + r"""class {{ interface.python_name }}( + DbusInterfaceCommon, + interface_name="{{ interface.interface_name }}", +): +{%- filter indent -%} +{% for method in interface.methods -%} +{% include 'blocking_method' %} +{% endfor -%} +{% for a_property in interface.properties -%} +{% include 'blocking_property' %} +{% endfor -%} +{%- endfilter -%} +""" + ), + "blocking_method": ( + r""" +@dbus_method( +{%- filter indent -%} +{%- include 'generic_method_flags' -%} +{%- endfilter %} +) +def {{ method.python_name }}( + self, + +{%- for arg_name, arg_type in method.args_names_and_typing %} + {{ arg_name }}: {{ arg_type }}, +{%- endfor %} +) -> {{ method.result_typing }}: + raise NotImplementedError +""" + ), + "blocking_property": ( + r""" +@dbus_property( +{%- filter indent -%} +{%- include 'generic_property_flags' -%} +{%- endfilter %} +) +def {{ a_property.python_name }}(self) -> {{ a_property.typing }}: + raise NotImplementedError""" + ), } @@ -690,15 +768,19 @@ def interfaces_from_str(xml_str: str) -> List[DbusInterfaceIntrospection]: return xml_to_interfaces_introspection(etree) -def generate_async_py_file( - interfaces: List[DbusInterfaceIntrospection], - include_import_header: bool = True) -> str: +def generate_py_file( + interfaces: List[DbusInterfaceIntrospection], + include_import_header: bool = True, + do_async: bool = True, +) -> str: from jinja2 import DictLoader from jinja2 import Environment as JinjaEnv + template_name = "async_main" if do_async else "blocking_main" + env = JinjaEnv(loader=DictLoader(INTERFACE_TEMPLATES)) - return env.get_template("async_main").render( + return env.get_template(template_name).render( interfaces=interfaces, include_import_header=include_import_header, ) diff --git a/test/test_interface_generator.py b/test/test_interface_generator.py index 9b17f48..1cfb075 100644 --- a/test/test_interface_generator.py +++ b/test/test_interface_generator.py @@ -27,7 +27,7 @@ from sdbus.interface_generator import ( DbusSigToTyping, camel_case_to_snake_case, - generate_async_py_file, + generate_py_file, interface_name_to_class, interfaces_from_str, ) @@ -196,7 +196,7 @@ def test_parsing(self) -> None: False, ) - generated = generate_async_py_file(interfaces_intro) + generated = generate_py_file(interfaces_intro) self.assertIn('flags=DbusPropertyEmitsInvalidationFlag', generated) self.assertIn('flags=DbusPropertyConstFlag', generated) @@ -228,6 +228,38 @@ def test_generate_from_connection(self) -> None: "get_connection_unix_process_id", generated_interface, ) + self.assertIn( + "async", + generated_interface, + ) + + def test_generate_from_connection_blocking(self) -> None: + if find_spec('jinja2') is None: + raise SkipTest('Jinja2 not installed') + + with patch("sdbus.__main__.stdout") as stdout_mock: + generator_main( + [ + "gen-from-connection", + "--block", + "org.freedesktop.DBus", + "/org/freedesktop/DBus", + ] + ) + + write_mock: MagicMock = stdout_mock.write + write_mock.assert_called_once() + + generated_interface = write_mock.call_args.args[0] + + self.assertNotIn( + "async", + generated_interface, + ) + self.assertIn( + "dbus_property", + generated_interface, + ) if __name__ == "__main__": From 3ab1f00e1f24921d6834011462543a1e7525b5dc Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sun, 29 Oct 2023 17:37:00 +0600 Subject: [PATCH 043/188] Output ... when generating interfaces without members Otherwise the body of the class will be empty which raises the syntax error. --- src/sdbus/interface_generator.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/sdbus/interface_generator.py b/src/sdbus/interface_generator.py index e9aac27..320b12f 100644 --- a/src/sdbus/interface_generator.py +++ b/src/sdbus/interface_generator.py @@ -540,6 +540,10 @@ def __init__(self, element: Element): else: raise ValueError(f'Unknown D-Bus member {dbus_member}') + @property + def has_members(self) -> bool: + return any((self.methods, self.properties, self.signals)) + SKIP_INTERFACES = { 'org.freedesktop.DBus.Properties', @@ -549,6 +553,7 @@ def __init__(self, element: Element): } INTERFACE_TEMPLATES: Dict[str, str] = { + "generic_no_members": r"... # Interface has no members", "generic_method_flags": ( r""" {%- if method.dbus_input_signature %} @@ -609,6 +614,7 @@ def __init__(self, element: Element): interface_name="{{ interface.interface_name }}", ): {%- filter indent -%} +{%- if interface.has_members -%} {% for method in interface.methods -%} {% include 'async_method' %} {% endfor -%} @@ -618,6 +624,9 @@ def __init__(self, element: Element): {% for signal in interface.signals -%} {% include 'async_signal' %} {% endfor -%} +{%- else %} +{% include 'generic_no_members' %} +{% endif -%} {%- endfilter -%} """ ), @@ -693,12 +702,16 @@ def {{ signal.python_name }}(self) -> {{ signal.typing }}: interface_name="{{ interface.interface_name }}", ): {%- filter indent -%} +{%- if interface.has_members -%} {% for method in interface.methods -%} {% include 'blocking_method' %} {% endfor -%} {% for a_property in interface.properties -%} {% include 'blocking_property' %} {% endfor -%} +{%- else %} +{% include 'generic_no_members' %} +{% endif -%} {%- endfilter -%} """ ), From 9040f153d9cb88ad066e52af712792419086d634 Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sun, 29 Oct 2023 18:17:14 +0600 Subject: [PATCH 044/188] Version 0.11.1 --- CHANGELOG.md | 24 ++++++++++++++++++++++++ setup.py | 2 +- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 85a9f5d..f286cd0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,27 @@ +## 0.11.1 + +### Features: + +* Improved interface generator handling of multiple uppercase letters + sequences. For example, `ACTIVATE_CONNECTION` would before be converted + to `a_c_t_i_v_a_t_e__c_o_n_n_e_c_t_i_o_n` and after to `activate_connection`. + (reported by @bhattarabi) +* Improved python formatting generated by interface code generator. +* Added option `--block` to generate blocking interface code. + (requested by @zhanglongqi and @MathisMARION) + +### Fixes: + +* Fixed docstrings still being present even if python was configured with + `--without-doc-strings`. +* Fixed interface generator crashing when a rare write-only property is + encountered. (reported by @gotthardp) +* Fixed async interfaces iterating over all members during initialization. + (reported by @gotthardp) +* Fixed `TypeError: Dbus type '\x00' is unknown` being raised when trying to read + from a message more than one time. (reported by @IB1387 and @asmello) +* Fixed missing class body when generating code for interface without members. + ## 0.11.0 ### Features: diff --git a/setup.py b/setup.py index 469dbb2..7b6a236 100644 --- a/setup.py +++ b/setup.py @@ -93,7 +93,7 @@ def get_link_arguments() -> List[str]: 'Based on sd-bus from libsystemd.'), long_description=long_description, long_description_content_type='text/markdown', - version='0.11.0', + version='0.11.1', url='https://github.com/igo95862/python-sdbus', author='igo95862', author_email='igo95862@yandex.ru', From d31110dc3f5c7106190ed1f8a5dca219dfad2021 Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sun, 5 Nov 2023 15:43:18 +0600 Subject: [PATCH 045/188] test: Rename old tests with updated names `sd_bus` -> `sdbus` `sync` -> `block` --- test/leak_tests.py | 2 +- test/{test_sd_bus_async.py => test_sdbus_async.py} | 0 test/{test_sd_bus_sync.py => test_sdbus_block.py} | 0 3 files changed, 1 insertion(+), 1 deletion(-) rename test/{test_sd_bus_async.py => test_sdbus_async.py} (100%) rename test/{test_sd_bus_sync.py => test_sdbus_block.py} (100%) diff --git a/test/leak_tests.py b/test/leak_tests.py index 03604e8..8fd1590 100644 --- a/test/leak_tests.py +++ b/test/leak_tests.py @@ -42,7 +42,7 @@ InterfaceWithErrors, ) from .test_read_write_dbus_types import TestDbusTypes -from .test_sd_bus_async import TestPing, TestProxy, initialize_object +from .test_sdbus_async import TestPing, TestProxy, initialize_object ENABLE_LEAK_TEST_VAR = 'PYTHON_SDBUS_TEST_LEAKS' diff --git a/test/test_sd_bus_async.py b/test/test_sdbus_async.py similarity index 100% rename from test/test_sd_bus_async.py rename to test/test_sdbus_async.py diff --git a/test/test_sd_bus_sync.py b/test/test_sdbus_block.py similarity index 100% rename from test/test_sd_bus_sync.py rename to test/test_sdbus_block.py From 468f4c6291ad1e1d60c9dd625f79e98d8d949a82 Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sun, 5 Nov 2023 16:26:31 +0600 Subject: [PATCH 046/188] test: Test bad blocking interface subclassing --- test/test_sdbus_block_bad_class.py | 78 ++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 test/test_sdbus_block_bad_class.py diff --git a/test/test_sdbus_block_bad_class.py b/test/test_sdbus_block_bad_class.py new file mode 100644 index 0000000..e6d1e75 --- /dev/null +++ b/test/test_sdbus_block_bad_class.py @@ -0,0 +1,78 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later + +# Copyright (C) 2023 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 unittest import TestCase +from unittest import main as unittest_main + +from sdbus import DbusInterfaceCommon, dbus_method, dbus_property + + +class GoodDbusInterface(DbusInterfaceCommon): + @dbus_method() + def test_method(self) -> None: + raise NotImplementedError + + @dbus_property("s") + def test_property(self) -> str: + return "test" + + +class TestBadDbusClass(TestCase): + def test_method_name_override(self) -> None: + with self.subTest("Method override"), self.assertRaises(TypeError): + + class BadMethodOverrideClass(GoodDbusInterface): + def test_method(self) -> None: + return + + with self.subTest("D-Bus method override"), self.assertRaises( + TypeError + ): + + class BadDbusMethodOverrideClass(GoodDbusInterface): + @dbus_method() + def test_method(self) -> None: + return + + with self.subTest("Property override"), self.assertRaises(TypeError): + + class BadPropertyOverrideClass(GoodDbusInterface): + def test_property(self) -> str: # type: ignore + return "override" + + with self.subTest("D-Bus property override"), self.assertRaises( + TypeError + ): + + class BadDbusPropertyOverrideClass(GoodDbusInterface): + @dbus_property("s") + def test_property(self) -> str: + return "override" + + with self.subTest("Good new method"): + + class GoodSubclass(GoodDbusInterface): + def new_method(self) -> int: + return 1 + + +if __name__ == "__main__": + unittest_main() From 44d84f03bfd0cd444a28586019e5f32d7cac0ed6 Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sun, 5 Nov 2023 17:13:33 +0600 Subject: [PATCH 047/188] test: Test blocking interface bad names --- test/test_sdbus_block_bad_class.py | 52 +++++++++++++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/test/test_sdbus_block_bad_class.py b/test/test_sdbus_block_bad_class.py index e6d1e75..8152134 100644 --- a/test/test_sdbus_block_bad_class.py +++ b/test/test_sdbus_block_bad_class.py @@ -19,10 +19,11 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA from __future__ import annotations -from unittest import TestCase +from unittest import SkipTest, TestCase from unittest import main as unittest_main from sdbus import DbusInterfaceCommon, dbus_method, dbus_property +from sdbus.sd_bus_internals import is_interface_name_valid class GoodDbusInterface(DbusInterfaceCommon): @@ -73,6 +74,55 @@ class GoodSubclass(GoodDbusInterface): def new_method(self) -> int: return 1 + def test_bad_class_names(self) -> None: + if not __debug__: + raise SkipTest("Assertions are not enabled") + + try: + is_interface_name_valid("org.test") + except NotImplementedError: + raise SkipTest("Validation functions not available") + + with self.assertRaisesRegex(AssertionError, "^Invalid interface name"): + + class BadInterfaceName( + DbusInterfaceCommon, + interface_name="0.test", + ): + ... + + with self.assertRaisesRegex( + AssertionError, + "^Invalid method name", + ): + + class BadMethodName( + DbusInterfaceCommon, + interface_name="org.example", + ): + @dbus_method( + result_signature="s", + method_name="🤫", + ) + def test(self) -> str: + return "test" + + with self.assertRaisesRegex( + AssertionError, + "^Invalid property name", + ): + + class BadPropertyName( + DbusInterfaceCommon, + interface_name="org.example", + ): + @dbus_property( + property_signature="s", + property_name="🤫", + ) + def test(self) -> str: + return "test" + if __name__ == "__main__": unittest_main() From dcf0bf0faeb4f7dc0db32c700eee18ea85567884 Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sun, 5 Nov 2023 17:45:58 +0600 Subject: [PATCH 048/188] test: Separate bad async subclass testing in to a file Subclass do not really need a D-Bus to run. --- test/common_test_util.py | 22 ++++++- test/test_sdbus_async.py | 77 ---------------------- test/test_sdbus_async_bad_class.py | 100 +++++++++++++++++++++++++++++ test/test_sdbus_block_bad_class.py | 13 ++-- 4 files changed, 125 insertions(+), 87 deletions(-) create mode 100644 test/test_sdbus_async_bad_class.py diff --git a/test/common_test_util.py b/test/common_test_util.py index b4316a6..9df7976 100644 --- a/test/common_test_util.py +++ b/test/common_test_util.py @@ -19,7 +19,7 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA from __future__ import annotations -from unittest import main +from unittest import SkipTest, main def mem_test() -> None: @@ -35,3 +35,23 @@ def mem_test_single(test_class: type, test_name: str) -> None: t = test_class() t.setUp() getattr(t, test_name)() + + +def skip_if_no_asserts() -> None: + try: + assert False + except AssertionError: + return + + raise SkipTest("Assertions are not enabled") + + +def skip_if_no_name_validations() -> None: + skip_if_no_asserts() + + from sdbus.sd_bus_internals import is_interface_name_valid + + try: + is_interface_name_valid("org.test") + except NotImplementedError: + raise SkipTest("Validation functions not available") diff --git a/test/test_sdbus_async.py b/test/test_sdbus_async.py index 554c77c..155dc63 100644 --- a/test/test_sdbus_async.py +++ b/test/test_sdbus_async.py @@ -40,7 +40,6 @@ DbusDeprecatedFlag, DbusPropertyConstFlag, DbusPropertyEmitsChangeFlag, - is_interface_name_valid, ) from sdbus.unittest import IsolatedDbusTestCase from sdbus.utils import parse_properties_changed @@ -754,82 +753,6 @@ def hello_world(self) -> str: test_object = EnumedInterfaceAsync() test_object.export_to_dbus(ObjectPathEnum.FOO) - async def test_name_validations(self) -> None: - if not __debug__: - raise SkipTest('Assertions are not enabled') - - try: - is_interface_name_valid('org.test') - except NotImplementedError: - raise SkipTest('Validation functions not available') - - def test_bad_interface_name() -> None: - class BadInterfaceName( - DbusInterfaceCommonAsync, - interface_name='0.test', - ): - ... - - self.assertRaisesRegex( - AssertionError, - '^Invalid interface name', - test_bad_interface_name, - ) - - def test_bad_method_name() -> None: - class BadMethodName( - DbusInterfaceCommonAsync, - interface_name='org.example', - ): - @dbus_method_async( - result_signature='s', - method_name='🤫', - ) - async def test(self) -> str: - return 'test' - - self.assertRaisesRegex( - AssertionError, - '^Invalid method name', - test_bad_method_name, - ) - - def test_bad_property_name() -> None: - class BadPropertyName( - DbusInterfaceCommonAsync, - interface_name='org.example', - ): - @dbus_property_async( - property_signature='s', - property_name='🤫', - ) - def test(self) -> str: - return 'test' - - self.assertRaisesRegex( - AssertionError, - '^Invalid property name', - test_bad_property_name, - ) - - def test_bad_signal_name() -> None: - class BadSignalName( - DbusInterfaceCommonAsync, - interface_name='org.example', - ): - @dbus_signal_async( - signal_signature='s', - signal_name='🤫', - ) - def test(self) -> str: - raise NotImplementedError - - self.assertRaisesRegex( - AssertionError, - '^Invalid signal name', - test_bad_signal_name, - ) - async def test_properties_get_all_dict(self) -> None: test_object, test_object_connection = initialize_object() diff --git a/test/test_sdbus_async_bad_class.py b/test/test_sdbus_async_bad_class.py new file mode 100644 index 0000000..0055eb8 --- /dev/null +++ b/test/test_sdbus_async_bad_class.py @@ -0,0 +1,100 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later + +# Copyright (C) 2023 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 unittest import TestCase +from unittest import main as unittest_main + +from sdbus import ( + DbusInterfaceCommonAsync, + dbus_method_async, + dbus_property_async, + dbus_signal_async, +) + +from .common_test_util import skip_if_no_name_validations + + +class TestBadAsyncDbusClass(TestCase): + def test_name_validations(self) -> None: + skip_if_no_name_validations() + + with self.assertRaisesRegex( + AssertionError, + "^Invalid interface name", + ): + + class BadInterfaceName( + DbusInterfaceCommonAsync, + interface_name="0.test", + ): + ... + + with self.assertRaisesRegex( + AssertionError, + "^Invalid method name", + ): + + class BadMethodName( + DbusInterfaceCommonAsync, + interface_name="org.example", + ): + @dbus_method_async( + result_signature="s", + method_name="🤫", + ) + async def test(self) -> str: + return "test" + + with self.assertRaisesRegex( + AssertionError, + "^Invalid property name", + ): + + class BadPropertyName( + DbusInterfaceCommonAsync, + interface_name="org.example", + ): + @dbus_property_async( + property_signature="s", + property_name="🤫", + ) + def test(self) -> str: + return "test" + + with self.assertRaisesRegex( + AssertionError, + "^Invalid signal name", + ): + + class BadSignalName( + DbusInterfaceCommonAsync, + interface_name="org.example", + ): + @dbus_signal_async( + signal_signature="s", + signal_name="🤫", + ) + def test(self) -> str: + raise NotImplementedError + + +if __name__ == "__main__": + unittest_main() diff --git a/test/test_sdbus_block_bad_class.py b/test/test_sdbus_block_bad_class.py index 8152134..0df0a5d 100644 --- a/test/test_sdbus_block_bad_class.py +++ b/test/test_sdbus_block_bad_class.py @@ -19,11 +19,12 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA from __future__ import annotations -from unittest import SkipTest, TestCase +from unittest import TestCase from unittest import main as unittest_main from sdbus import DbusInterfaceCommon, dbus_method, dbus_property -from sdbus.sd_bus_internals import is_interface_name_valid + +from .common_test_util import skip_if_no_name_validations class GoodDbusInterface(DbusInterfaceCommon): @@ -75,13 +76,7 @@ def new_method(self) -> int: return 1 def test_bad_class_names(self) -> None: - if not __debug__: - raise SkipTest("Assertions are not enabled") - - try: - is_interface_name_valid("org.test") - except NotImplementedError: - raise SkipTest("Validation functions not available") + skip_if_no_name_validations() with self.assertRaisesRegex(AssertionError, "^Invalid interface name"): From b90a5a3b051322d83a3e5b1a2d3912340a4cb60e Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sun, 5 Nov 2023 18:37:22 +0600 Subject: [PATCH 049/188] test: Move more class definitions tests to separate file --- test/leak_tests.py | 1 - test/test_sdbus_async.py | 60 ----------------------- test/test_sdbus_async_bad_class.py | 77 +++++++++++++++++++++++++++++- 3 files changed, 76 insertions(+), 62 deletions(-) diff --git a/test/leak_tests.py b/test/leak_tests.py index 8fd1590..3326f43 100644 --- a/test/leak_tests.py +++ b/test/leak_tests.py @@ -116,7 +116,6 @@ async def test_objects(self) -> None: await TestProxy.test_method_kwargs(pseudo_test) await TestProxy.test_method(pseudo_test) await TestProxy.test_subclass(pseudo_test) - await TestProxy.test_bad_subclass(pseudo_test) await TestProxy.test_properties(pseudo_test) await TestProxy.test_signal(pseudo_test) await TestProxy.test_exceptions(pseudo_test) diff --git a/test/test_sdbus_async.py b/test/test_sdbus_async.py index 155dc63..794fec1 100644 --- a/test/test_sdbus_async.py +++ b/test/test_sdbus_async.py @@ -25,7 +25,6 @@ from typing import Tuple, cast from unittest import SkipTest -from sdbus.dbus_common_funcs import PROPERTY_FLAGS_MASK, count_bits from sdbus.dbus_proxy_async_interfaces import DBUS_PROPERTIES_CHANGED_TYPING from sdbus.exceptions import ( DbusFailedError, @@ -37,8 +36,6 @@ ) from sdbus.sd_bus_internals import ( DBUS_ERROR_TO_EXCEPTION, - DbusDeprecatedFlag, - DbusPropertyConstFlag, DbusPropertyEmitsChangeFlag, ) from sdbus.unittest import IsolatedDbusTestCase @@ -384,18 +381,6 @@ def test_property(self) -> str: self.assertEqual( await test_subclass_tri_connection.test_property, 'tri') - async def test_bad_subclass(self) -> None: - with self.assertRaises(TypeError): - class TestInheritence(TestInterface): - async def test_int(self) -> int: - return 2 - - with self.assertRaises(TypeError): - class TestInheritence2(TestInterface): - @dbus_method_async_override() - async def test_unrelated(self) -> int: - return 2 - async def test_properties(self) -> None: test_object, test_object_connection = initialize_object() @@ -661,51 +646,6 @@ async def catch_property_emit_local() -> str: self.assertEqual(t1_result, test_str) self.assertEqual(t2_result, test_str) - async def test_property_flags(self) -> None: - self.assertEqual(0, PROPERTY_FLAGS_MASK & DbusDeprecatedFlag) - self.assertEqual( - 1, - count_bits(PROPERTY_FLAGS_MASK & (DbusDeprecatedFlag - | DbusPropertyEmitsChangeFlag)) - ) - self.assertEqual( - 2, - count_bits( - PROPERTY_FLAGS_MASK & ( - DbusDeprecatedFlag | - DbusPropertyConstFlag | - DbusPropertyEmitsChangeFlag))) - - def must_raise_value_error() -> None: - class InvalidPropertiesFlags( - DbusInterfaceCommonAsync, - interface_name='org.test.test'): - @dbus_property_async( - "s", - flags=DbusPropertyConstFlag | DbusPropertyEmitsChangeFlag, - ) - def test_constant(self) -> str: - return "a" - - self.assertRaisesRegex( - AssertionError, - '^Incorrect number of Property flags', - must_raise_value_error, - ) - - def should_be_no_error() -> None: - class ValidPropertiesFlags( - DbusInterfaceCommonAsync, - interface_name='org.test.test'): - @dbus_property_async( - "s", - flags=DbusDeprecatedFlag | DbusPropertyEmitsChangeFlag, - ) - def test_constant(self) -> str: - return "a" - - should_be_no_error() - async def test_bus_close(self) -> None: test_object, test_object_connection = initialize_object() diff --git a/test/test_sdbus_async_bad_class.py b/test/test_sdbus_async_bad_class.py index 0055eb8..5c3d60f 100644 --- a/test/test_sdbus_async_bad_class.py +++ b/test/test_sdbus_async_bad_class.py @@ -22,14 +22,26 @@ from unittest import TestCase from unittest import main as unittest_main +from sdbus.dbus_common_funcs import PROPERTY_FLAGS_MASK, count_bits + from sdbus import ( + DbusDeprecatedFlag, DbusInterfaceCommonAsync, + DbusPropertyConstFlag, + DbusPropertyEmitsChangeFlag, dbus_method_async, + dbus_method_async_override, dbus_property_async, dbus_signal_async, ) -from .common_test_util import skip_if_no_name_validations +from .common_test_util import skip_if_no_asserts, skip_if_no_name_validations + + +class TestInterface(DbusInterfaceCommonAsync): + @dbus_method_async(result_signature="i") + async def test_int(self) -> int: + return 1 class TestBadAsyncDbusClass(TestCase): @@ -95,6 +107,69 @@ class BadSignalName( def test(self) -> str: raise NotImplementedError + def test_property_flags(self) -> None: + self.assertEqual(0, PROPERTY_FLAGS_MASK & DbusDeprecatedFlag) + self.assertEqual( + 1, + count_bits( + PROPERTY_FLAGS_MASK + & (DbusDeprecatedFlag | DbusPropertyEmitsChangeFlag) + ), + ) + self.assertEqual( + 2, + count_bits( + PROPERTY_FLAGS_MASK + & ( + DbusDeprecatedFlag + | DbusPropertyConstFlag + | DbusPropertyEmitsChangeFlag + ) + ), + ) + + with self.subTest("Test incorrect flags"), self.assertRaisesRegex( + AssertionError, + "^Incorrect number of Property flags", + ): + skip_if_no_asserts() + + class InvalidPropertiesFlags( + DbusInterfaceCommonAsync, interface_name="org.test.test" + ): + @dbus_property_async( + "s", + flags=DbusPropertyConstFlag | DbusPropertyEmitsChangeFlag, + ) + def test_constant(self) -> str: + return "a" + + with self.subTest("Valid properties flags"): + + class ValidPropertiesFlags( + DbusInterfaceCommonAsync, interface_name="org.test.test" + ): + @dbus_property_async( + "s", + flags=DbusDeprecatedFlag | DbusPropertyEmitsChangeFlag, + ) + def test_constant(self) -> str: + return "a" + + def test_bad_subclass(self) -> None: + with self.assertRaises(TypeError): + + class TestInheritence(TestInterface): + async def test_int(self) -> int: + return 2 + + with self.assertRaises(TypeError): + + class TestInheritence2(TestInterface): + @dbus_method_async_override() + async def test_unrelated(self) -> int: + return 2 + if __name__ == "__main__": unittest_main() From 838a373feba0e88cac364965d8a146495f67e136 Mon Sep 17 00:00:00 2001 From: igo95862 Date: Mon, 20 Nov 2023 23:00:52 +0600 Subject: [PATCH 050/188] Add repology widget to README to display packaging status Thank you @bluca for packaing python-sdbus for Debian. --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index f58a722..707135e 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,10 @@ # Modern Python library for D-Bus + + Packaging status + + Features: * Asyncio and blocking calls. From effe5d824b5e5fef0186f5a785e4e5f0ed67fefa Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sun, 26 Nov 2023 23:22:08 +0600 Subject: [PATCH 051/188] Clean-up existing typing Put unnecessary run-time imports in to TYPE_CHECKING if. Only use a single T TypeVar. Make `new_proxy` not require typing assert with the bounded TypeVar. --- src/sdbus/__main__.py | 8 +++- src/sdbus/autodoc.py | 9 ++-- src/sdbus/dbus_common_elements.py | 30 +++++++------- src/sdbus/dbus_common_funcs.py | 8 +++- src/sdbus/dbus_exceptions.py | 20 ++++++--- src/sdbus/dbus_proxy_async_interface_base.py | 38 +++++++---------- src/sdbus/dbus_proxy_async_interfaces.py | 21 ++++++---- src/sdbus/dbus_proxy_async_method.py | 37 +++++++---------- src/sdbus/dbus_proxy_async_property.py | 22 ++++------ src/sdbus/dbus_proxy_async_signal.py | 32 +++++++-------- src/sdbus/dbus_proxy_sync_interface_base.py | 8 +++- src/sdbus/dbus_proxy_sync_interfaces.py | 5 ++- src/sdbus/dbus_proxy_sync_method.py | 28 ++++--------- src/sdbus/dbus_proxy_sync_property.py | 19 +++------ src/sdbus/interface_generator.py | 25 +++++++----- src/sdbus/sd_bus_internals.py | 43 +++++++++++--------- src/sdbus/unittest.py | 6 ++- src/sdbus/utils.py | 30 ++++++++------ test/test_sdbus_async.py | 12 +++++- 19 files changed, 206 insertions(+), 195 deletions(-) diff --git a/src/sdbus/__main__.py b/src/sdbus/__main__.py index b0e8c61..b1cb494 100644 --- a/src/sdbus/__main__.py +++ b/src/sdbus/__main__.py @@ -22,15 +22,19 @@ from argparse import ArgumentParser from pathlib import Path from sys import stdout -from typing import List, Optional +from typing import TYPE_CHECKING from .interface_generator import ( - DbusInterfaceIntrospection, generate_py_file, interfaces_from_file, interfaces_from_str, ) +if TYPE_CHECKING: + from typing import List, Optional + + from .interface_generator import DbusInterfaceIntrospection + def run_gen_from_connection( connection_name: str, diff --git a/src/sdbus/autodoc.py b/src/sdbus/autodoc.py index 8a709ea..3e3b054 100644 --- a/src/sdbus/autodoc.py +++ b/src/sdbus/autodoc.py @@ -17,12 +17,10 @@ # 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 typing import Any, Dict +from typing import TYPE_CHECKING -from sphinx.application import Sphinx from sphinx.ext.autodoc import AttributeDocumenter, MethodDocumenter from .dbus_proxy_async_method import DbusMethodAsyncBinded @@ -32,6 +30,11 @@ ) from .dbus_proxy_async_signal import DbusSignalAsync, DbusSignalBinded +if TYPE_CHECKING: + from typing import Any, Dict + + from sphinx.application import Sphinx + class DbusMethodDocumenter(MethodDocumenter): diff --git a/src/sdbus/dbus_common_elements.py b/src/sdbus/dbus_common_elements.py index 2dd19e9..e5c73f3 100644 --- a/src/sdbus/dbus_common_elements.py +++ b/src/sdbus/dbus_common_elements.py @@ -20,17 +20,7 @@ from __future__ import annotations from inspect import getfullargspec -from types import FunctionType -from typing import ( - Any, - Callable, - Dict, - List, - Optional, - Sequence, - Tuple, - TypeVar, -) +from typing import TYPE_CHECKING from .dbus_common_funcs import ( _is_property_flags_correct, @@ -38,6 +28,21 @@ ) from .sd_bus_internals import is_interface_name_valid, is_member_name_valid +if TYPE_CHECKING: + from types import FunctionType + from typing import ( + Any, + Callable, + Dict, + List, + Optional, + Sequence, + Tuple, + TypeVar, + ) + + T = TypeVar('T') + class DbusSomethingCommon: def __init__(self) -> None: @@ -269,9 +274,6 @@ class DbusBindedSync: ... -T = TypeVar('T') - - class DbusOverload: def __init__(self, original: T): self.original = original diff --git a/src/sdbus/dbus_common_funcs.py b/src/sdbus/dbus_common_funcs.py index 3aa5095..6987396 100644 --- a/src/sdbus/dbus_common_funcs.py +++ b/src/sdbus/dbus_common_funcs.py @@ -22,7 +22,7 @@ from asyncio import Future, get_running_loop from contextvars import ContextVar -from typing import Any, Dict, Generator, Iterator, Literal, Tuple +from typing import TYPE_CHECKING from warnings import warn from .sd_bus_internals import ( @@ -33,10 +33,14 @@ NameAllowReplacementFlag, NameQueueFlag, NameReplaceExistingFlag, - SdBus, sd_bus_open, ) +if TYPE_CHECKING: + from typing import Any, Dict, Generator, Iterator, Literal, Tuple + + from .sd_bus_internals import SdBus + DEFAULT_BUS: ContextVar[SdBus] = ContextVar('DEFAULT_BUS') PROPERTY_FLAGS_MASK = ( diff --git a/src/sdbus/dbus_exceptions.py b/src/sdbus/dbus_exceptions.py index a78f1fb..b5d4544 100644 --- a/src/sdbus/dbus_exceptions.py +++ b/src/sdbus/dbus_exceptions.py @@ -19,7 +19,7 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA from __future__ import annotations -from typing import Any, Dict, Tuple, cast +from typing import TYPE_CHECKING from .sd_bus_internals import ( SdBusBaseError, @@ -27,13 +27,18 @@ map_exception_to_dbus_error, ) +if TYPE_CHECKING: + from typing import Any, Dict, Tuple + class DbusErrorMeta(type): - def __new__(cls, name: str, - bases: Tuple[type, ...], - namespace: Dict[str, Any], - ) -> DbusErrorMeta: + def __new__( + cls, + name: str, + bases: Tuple[type, ...], + namespace: Dict[str, Any], + ) -> DbusErrorMeta: dbus_error_name = namespace.get('dbus_error_name') @@ -41,8 +46,11 @@ def __new__(cls, name: str, raise TypeError('D-Bus error name not passed') new_cls = super().__new__(cls, name, bases, namespace) + assert issubclass(new_cls, Exception), ( + f"New class {new_cls} is not an Exception but {bases}." + ) - add_exception_mapping(cast(Exception, new_cls)) + add_exception_mapping(new_cls) return new_cls diff --git a/src/sdbus/dbus_proxy_async_interface_base.py b/src/sdbus/dbus_proxy_async_interface_base.py index 0a3dd7c..2dc8d92 100644 --- a/src/sdbus/dbus_proxy_async_interface_base.py +++ b/src/sdbus/dbus_proxy_async_interface_base.py @@ -19,27 +19,14 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA from __future__ import annotations -from asyncio import Queue from copy import deepcopy from inspect import getmembers from types import MethodType -from typing import ( - Any, - Callable, - Dict, - List, - Optional, - Set, - Tuple, - Type, - TypeVar, - cast, -) +from typing import TYPE_CHECKING, Any, Callable, cast from warnings import warn from weakref import ref as weak_ref from .dbus_common_elements import ( - DbusBindedAsync, DbusInterfaceMetaCommon, DbusOverload, DbusSomethingAsync, @@ -52,9 +39,16 @@ DbusPropertyAsyncBinded, ) from .dbus_proxy_async_signal import DbusSignalAsync, DbusSignalBinded -from .sd_bus_internals import SdBus, SdBusInterface +from .sd_bus_internals import SdBusInterface + +if TYPE_CHECKING: + from asyncio import Queue + from typing import Dict, List, Optional, Set, Tuple, Type, TypeVar + + from .dbus_common_elements import DbusBindedAsync + from .sd_bus_internals import SdBus -T_input = TypeVar('T_input') + Self = TypeVar('Self', bound="DbusInterfaceBaseAsync") class DbusInterfaceMetaAsync(DbusInterfaceMetaCommon): @@ -296,32 +290,28 @@ def _proxify( @classmethod def new_connect( - cls: Type[T_input], + cls: Type[Self], service_name: str, object_path: str, bus: Optional[SdBus] = None, - ) -> T_input: + ) -> Self: warn( ("new_connect is deprecated in favor of equivalent new_proxy." "Will be removed in version 1.0.0"), DeprecationWarning, ) new_object = cls.__new__(cls) - assert isinstance(new_object, DbusInterfaceBaseAsync) new_object._proxify(service_name, object_path, bus) - assert isinstance(new_object, cls) return new_object @classmethod def new_proxy( - cls: Type[T_input], + cls: Type[Self], service_name: str, object_path: str, bus: Optional[SdBus] = None, - ) -> T_input: + ) -> Self: new_object = cls.__new__(cls) - assert isinstance(new_object, DbusInterfaceBaseAsync) new_object._proxify(service_name, object_path, bus) - assert isinstance(new_object, cls) return new_object diff --git a/src/sdbus/dbus_proxy_async_interfaces.py b/src/sdbus/dbus_proxy_async_interfaces.py index 3f21171..607f855 100644 --- a/src/sdbus/dbus_proxy_async_interfaces.py +++ b/src/sdbus/dbus_proxy_async_interfaces.py @@ -19,13 +19,25 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA from __future__ import annotations -from typing import Any, Dict, List, Literal, Optional, Tuple +from typing import TYPE_CHECKING from .dbus_common_funcs import _parse_properties_vardict, get_default_bus from .dbus_proxy_async_interface_base import DbusInterfaceBaseAsync from .dbus_proxy_async_method import dbus_method_async from .dbus_proxy_async_signal import dbus_signal_async -from .sd_bus_internals import SdBus, SdBusSlot + +if TYPE_CHECKING: + from typing import Any, Dict, List, Literal, Optional, Tuple + + from .sd_bus_internals import SdBus, SdBusSlot + + DBUS_PROPERTIES_CHANGED_TYPING = ( + Tuple[ + str, + Dict[str, Tuple[str, Any]], + List[str], + ] + ) class DbusPeerInterfaceAsync( @@ -54,11 +66,6 @@ async def dbus_introspect(self) -> str: raise NotImplementedError -DBUS_PROPERTIES_CHANGED_TYPING = Tuple[str, - Dict[str, Tuple[str, Any]], - List[str]] - - class DbusPropertiesInterfaceAsync( DbusInterfaceBaseAsync, interface_name='org.freedesktop.DBus.Properties', diff --git a/src/sdbus/dbus_proxy_async_method.py b/src/sdbus/dbus_proxy_async_method.py index d42a405..0c5214f 100644 --- a/src/sdbus/dbus_proxy_async_method.py +++ b/src/sdbus/dbus_proxy_async_method.py @@ -22,16 +22,7 @@ from contextvars import ContextVar, copy_context from inspect import iscoroutinefunction from types import FunctionType -from typing import ( - TYPE_CHECKING, - Any, - Callable, - Optional, - Sequence, - Type, - TypeVar, - cast, -) +from typing import TYPE_CHECKING, cast from weakref import ref as weak_ref from .dbus_common_elements import ( @@ -41,21 +32,23 @@ DbusSomethingAsync, ) from .dbus_exceptions import DbusFailedError -from .sd_bus_internals import DbusNoReplyFlag, SdBusMessage - -CURRENT_MESSAGE: ContextVar[SdBusMessage] = ContextVar('CURRENT_MESSAGE') +from .sd_bus_internals import DbusNoReplyFlag +if TYPE_CHECKING: + from typing import Any, Callable, Optional, Sequence, Type, TypeVar -def get_current_message() -> SdBusMessage: - return CURRENT_MESSAGE.get() + from .dbus_proxy_async_interface_base import DbusInterfaceBaseAsync + from .sd_bus_internals import SdBusMessage + T = TypeVar('T') +else: + T = None -T_input = TypeVar('T_input') -T = TypeVar('T') +CURRENT_MESSAGE: ContextVar[SdBusMessage] = ContextVar('CURRENT_MESSAGE') -if TYPE_CHECKING: - from .dbus_proxy_async_interface_base import DbusInterfaceBaseAsync +def get_current_message() -> SdBusMessage: + return CURRENT_MESSAGE.get() class DbusMethodAsync(DbusMethodCommon, DbusSomethingAsync): @@ -212,14 +205,14 @@ def dbus_method_async( result_args_names: Sequence[str] = (), input_args_names: Sequence[str] = (), method_name: Optional[str] = None, -) -> Callable[[T_input], T_input]: +) -> Callable[[T], T]: assert not isinstance(input_signature, FunctionType), ( "Passed function to decorator directly. " "Did you forget () round brackets?" ) - def dbus_method_decorator(original_method: T_input) -> T_input: + def dbus_method_decorator(original_method: T) -> T: assert isinstance(original_method, FunctionType) assert iscoroutinefunction(original_method), ( "Expected coroutine function. ", @@ -235,7 +228,7 @@ def dbus_method_decorator(original_method: T_input) -> T_input: flags=flags, ) - return cast(T_input, new_wrapper) + return cast(T, new_wrapper) return dbus_method_decorator diff --git a/src/sdbus/dbus_proxy_async_property.py b/src/sdbus/dbus_proxy_async_property.py index 08631e1..5928a2a 100644 --- a/src/sdbus/dbus_proxy_async_property.py +++ b/src/sdbus/dbus_proxy_async_property.py @@ -21,17 +21,7 @@ from inspect import iscoroutinefunction from types import FunctionType -from typing import ( - TYPE_CHECKING, - Any, - Callable, - Generator, - Generic, - Optional, - Type, - TypeVar, - cast, -) +from typing import TYPE_CHECKING, Generic, TypeVar, cast from weakref import ref as weak_ref from .dbus_common_elements import ( @@ -40,13 +30,15 @@ DbusPropertyCommon, DbusSomethingAsync, ) -from .sd_bus_internals import SdBusMessage - -T = TypeVar('T') - if TYPE_CHECKING: + from typing import Any, Callable, Generator, Optional, Type + from .dbus_proxy_async_interface_base import DbusInterfaceBaseAsync + from .sd_bus_internals import SdBusMessage + + +T = TypeVar('T') class DbusPropertyAsync(DbusSomethingAsync, DbusPropertyCommon, Generic[T]): diff --git a/src/sdbus/dbus_proxy_async_signal.py b/src/sdbus/dbus_proxy_async_signal.py index be8d8f2..0c3bbc3 100644 --- a/src/sdbus/dbus_proxy_async_signal.py +++ b/src/sdbus/dbus_proxy_async_signal.py @@ -21,19 +21,7 @@ from asyncio import Queue from types import FunctionType -from typing import ( - TYPE_CHECKING, - Any, - AsyncGenerator, - Callable, - Generic, - Optional, - Sequence, - Tuple, - Type, - TypeVar, - cast, -) +from typing import TYPE_CHECKING, Generic, TypeVar, cast from weakref import ref as weak_ref from .dbus_common_elements import ( @@ -42,13 +30,23 @@ DbusSomethingAsync, ) from .dbus_common_funcs import get_default_bus -from .sd_bus_internals import SdBus, SdBusMessage - -T = TypeVar('T') - if TYPE_CHECKING: + from typing import ( + Any, + AsyncGenerator, + Callable, + Optional, + Sequence, + Tuple, + Type, + ) + from .dbus_proxy_async_interface_base import DbusInterfaceBaseAsync + from .sd_bus_internals import SdBus, SdBusMessage + + +T = TypeVar('T') class DbusSignalAsync(DbusSomethingAsync, DbusSingalCommon, Generic[T]): diff --git a/src/sdbus/dbus_proxy_sync_interface_base.py b/src/sdbus/dbus_proxy_sync_interface_base.py index eb8d5a0..3624d5d 100644 --- a/src/sdbus/dbus_proxy_sync_interface_base.py +++ b/src/sdbus/dbus_proxy_sync_interface_base.py @@ -19,7 +19,7 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA from __future__ import annotations -from typing import Any, Dict, Optional, Set, Tuple, cast +from typing import TYPE_CHECKING, cast from .dbus_common_elements import ( DbusInterfaceMetaCommon, @@ -29,7 +29,11 @@ from .dbus_common_funcs import get_default_bus from .dbus_proxy_sync_method import DbusMethodSync from .dbus_proxy_sync_property import DbusPropertySync -from .sd_bus_internals import SdBus + +if TYPE_CHECKING: + from typing import Any, Dict, Optional, Set, Tuple + + from .sd_bus_internals import SdBus class DbusInterfaceMetaSync(DbusInterfaceMetaCommon): diff --git a/src/sdbus/dbus_proxy_sync_interfaces.py b/src/sdbus/dbus_proxy_sync_interfaces.py index 7d9dc15..0c72ff6 100644 --- a/src/sdbus/dbus_proxy_sync_interfaces.py +++ b/src/sdbus/dbus_proxy_sync_interfaces.py @@ -19,11 +19,14 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA from __future__ import annotations -from typing import Any, Dict, Literal, Tuple +from typing import TYPE_CHECKING from .dbus_proxy_sync_interface_base import DbusInterfaceBase from .dbus_proxy_sync_method import dbus_method +if TYPE_CHECKING: + from typing import Any, Dict, Literal, Tuple + class DbusPeerInterface( DbusInterfaceBase, diff --git a/src/sdbus/dbus_proxy_sync_method.py b/src/sdbus/dbus_proxy_sync_method.py index 277f781..be0ab7f 100644 --- a/src/sdbus/dbus_proxy_sync_method.py +++ b/src/sdbus/dbus_proxy_sync_method.py @@ -21,33 +21,21 @@ from inspect import iscoroutinefunction from types import FunctionType -from typing import ( - TYPE_CHECKING, - Any, - Callable, - Optional, - Sequence, - Type, - TypeVar, - cast, -) +from typing import TYPE_CHECKING, TypeVar, cast from .dbus_common_elements import ( DbusBindedSync, DbusMethodCommon, DbusSomethingSync, ) -from .sd_bus_internals import SdBus - -DEFAULT_BUS: Optional[SdBus] = None - - -T_input = TypeVar('T_input') - if TYPE_CHECKING: + from typing import Any, Callable, Optional, Sequence, Type + from .dbus_proxy_sync_interface_base import DbusInterfaceBase +T = TypeVar('T') + class DbusMethodSync(DbusMethodCommon, DbusSomethingSync): def __get__(self, @@ -103,13 +91,13 @@ def dbus_method( result_signature: str = "", flags: int = 0, method_name: Optional[str] = None, -) -> Callable[[T_input], T_input]: +) -> Callable[[T], T]: assert not isinstance(input_signature, FunctionType), ( "Passed function to decorator directly. " "Did you forget () round brackets?" ) - def dbus_method_decorator(original_method: T_input) -> T_input: + def dbus_method_decorator(original_method: T) -> T: assert isinstance(original_method, FunctionType) assert not iscoroutinefunction(original_method), ( "Expected NON coroutine function. ", @@ -125,6 +113,6 @@ def dbus_method_decorator(original_method: T_input) -> T_input: flags=flags, ) - return cast(T_input, new_wrapper) + return cast(T, new_wrapper) return dbus_method_decorator diff --git a/src/sdbus/dbus_proxy_sync_property.py b/src/sdbus/dbus_proxy_sync_property.py index e0167f7..832f8ef 100644 --- a/src/sdbus/dbus_proxy_sync_property.py +++ b/src/sdbus/dbus_proxy_sync_property.py @@ -21,27 +21,20 @@ from inspect import iscoroutinefunction from types import FunctionType -from typing import ( - TYPE_CHECKING, - Any, - Callable, - Generic, - Optional, - Type, - TypeVar, - cast, -) +from typing import TYPE_CHECKING, Generic, TypeVar, cast from .dbus_common_elements import DbusPropertyCommon, DbusSomethingSync from .dbus_common_funcs import _check_sync_in_async_env -T = TypeVar('T') - - if TYPE_CHECKING: + from typing import Any, Callable, Optional, Type + from .dbus_proxy_sync_interface_base import DbusInterfaceBase +T = TypeVar('T') + + class DbusPropertySync(DbusPropertyCommon, DbusSomethingSync, Generic[T]): def __init__( self, diff --git a/src/sdbus/interface_generator.py b/src/sdbus/interface_generator.py index 320b12f..3d7a4a3 100644 --- a/src/sdbus/interface_generator.py +++ b/src/sdbus/interface_generator.py @@ -20,20 +20,23 @@ from __future__ import annotations from pathlib import Path -from typing import ( - Dict, - Iterable, - Iterator, - List, - Literal, - Optional, - Tuple, - Union, -) -from xml.etree.ElementTree import Element +from typing import TYPE_CHECKING from xml.etree.ElementTree import fromstring as etree_from_str from xml.etree.ElementTree import parse as etree_from_file +if TYPE_CHECKING: + from typing import ( + Dict, + Iterable, + Iterator, + List, + Literal, + Optional, + Tuple, + Union, + ) + from xml.etree.ElementTree import Element + def _camel_case_to_snake_case_generator(camel: str) -> Iterator[str]: i = iter(camel) diff --git a/src/sdbus/sd_bus_internals.py b/src/sdbus/sd_bus_internals.py index cdc5924..7e2d806 100644 --- a/src/sdbus/sd_bus_internals.py +++ b/src/sdbus/sd_bus_internals.py @@ -20,26 +20,29 @@ from __future__ import annotations from asyncio import Future, Queue -from typing import ( - Any, - Callable, - Coroutine, - Dict, - List, - Optional, - Sequence, - Tuple, - Type, - Union, -) - -DbusBasicTypes = Union[str, int, bytes, float, Any] -DbusStructType = Tuple[DbusBasicTypes, ...] -DbusDictType = Dict[DbusBasicTypes, DbusBasicTypes] -DbusVariantType = Tuple[str, DbusStructType] -DbusListType = List[DbusBasicTypes] -DbusCompleteTypes = Union[DbusBasicTypes, DbusStructType, - DbusDictType, DbusVariantType, DbusListType] +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import ( + Any, + Callable, + Coroutine, + Dict, + List, + Optional, + Sequence, + Tuple, + Type, + Union, + ) + + DbusBasicTypes = Union[str, int, bytes, float, Any] + DbusStructType = Tuple[DbusBasicTypes, ...] + DbusDictType = Dict[DbusBasicTypes, DbusBasicTypes] + DbusVariantType = Tuple[str, DbusStructType] + DbusListType = List[DbusBasicTypes] + DbusCompleteTypes = Union[DbusBasicTypes, DbusStructType, + DbusDictType, DbusVariantType, DbusListType] __STUB_ERROR = ( 'Typing stub. You should never see this ' diff --git a/src/sdbus/unittest.py b/src/sdbus/unittest.py index 4230f0e..e90698a 100644 --- a/src/sdbus/unittest.py +++ b/src/sdbus/unittest.py @@ -25,11 +25,15 @@ from subprocess import DEVNULL from subprocess import run as subprocess_run from tempfile import TemporaryDirectory -from typing import ClassVar +from typing import TYPE_CHECKING from unittest import IsolatedAsyncioTestCase from sdbus import sd_bus_open_user, set_default_bus +if TYPE_CHECKING: + from typing import ClassVar + + dbus_config = ''' session diff --git a/src/sdbus/utils.py b/src/sdbus/utils.py index cffb343..fa106fd 100644 --- a/src/sdbus/utils.py +++ b/src/sdbus/utils.py @@ -19,22 +19,26 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA from __future__ import annotations -from typing import ( - Any, - Dict, - FrozenSet, - Iterable, - List, - Literal, - Optional, - Tuple, - Type, - Union, -) +from typing import TYPE_CHECKING from .dbus_common_funcs import _parse_properties_vardict from .dbus_proxy_async_interface_base import DbusInterfaceBaseAsync -from .dbus_proxy_async_interfaces import DBUS_PROPERTIES_CHANGED_TYPING + +if TYPE_CHECKING: + from typing import ( + Any, + Dict, + FrozenSet, + Iterable, + List, + Literal, + Optional, + Tuple, + Type, + Union, + ) + + from .dbus_proxy_async_interfaces import DBUS_PROPERTIES_CHANGED_TYPING def parse_properties_changed( diff --git a/test/test_sdbus_async.py b/test/test_sdbus_async.py index 794fec1..3ec3acc 100644 --- a/test/test_sdbus_async.py +++ b/test/test_sdbus_async.py @@ -22,10 +22,9 @@ from asyncio import Event, get_running_loop, sleep, wait_for from asyncio.subprocess import create_subprocess_exec -from typing import Tuple, cast +from typing import TYPE_CHECKING, cast from unittest import SkipTest -from sdbus.dbus_proxy_async_interfaces import DBUS_PROPERTIES_CHANGED_TYPING from sdbus.exceptions import ( DbusFailedError, DbusFileExistsError, @@ -52,6 +51,15 @@ get_current_message, ) +if TYPE_CHECKING: + from typing import Tuple + + from sdbus.dbus_proxy_async_interfaces import ( + DBUS_PROPERTIES_CHANGED_TYPING, + ) +else: + DBUS_PROPERTIES_CHANGED_TYPING = None + class TestPing(IsolatedDbusTestCase): From c1d87bf4b25d50cf39b6cad3507d29288e4b60be Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sat, 2 Dec 2023 17:17:31 +0600 Subject: [PATCH 052/188] Make D-Bus element objects always have `interface_name` attribute Use the common metaclass to set the value of `interface_name`. If the D-Bus element is defined in the class without the interface name passed raise a TypeError. --- src/sdbus/dbus_common_elements.py | 22 +++++++++++++++--- src/sdbus/dbus_proxy_async_interface_base.py | 2 -- src/sdbus/dbus_proxy_async_method.py | 1 - src/sdbus/dbus_proxy_async_property.py | 3 --- src/sdbus/dbus_proxy_async_signal.py | 2 -- src/sdbus/dbus_proxy_sync_interface_base.py | 2 -- src/sdbus/dbus_proxy_sync_method.py | 1 - src/sdbus/dbus_proxy_sync_property.py | 2 -- test/test_sdbus_async_bad_class.py | 24 +++++++++++++++++--- test/test_sdbus_block_bad_class.py | 13 ++++++++++- 10 files changed, 52 insertions(+), 20 deletions(-) diff --git a/src/sdbus/dbus_common_elements.py b/src/sdbus/dbus_common_elements.py index e5c73f3..45e1204 100644 --- a/src/sdbus/dbus_common_elements.py +++ b/src/sdbus/dbus_common_elements.py @@ -45,9 +45,8 @@ class DbusSomethingCommon: - def __init__(self) -> None: - self.interface_name: Optional[str] = None - self.serving_enabled: bool = True + interface_name: str + serving_enabled: bool class DbusSomethingAsync(DbusSomethingCommon): @@ -78,6 +77,23 @@ def __new__(cls, name: str, except NotImplementedError: ... + for attr_name, attr in namespace.items(): + if not isinstance(attr, DbusSomethingCommon): + continue + + # TODO: Fix async metaclass copying all methods + if hasattr(attr, "interface_name"): + continue + + if interface_name is None: + raise TypeError( + f"Defined D-Bus element {attr_name!r} without " + f"interface name in the class {name!r}." + ) + + attr.interface_name = interface_name + attr.serving_enabled = serving_enabled + new_cls = super().__new__(cls, name, bases, namespace) return new_cls diff --git a/src/sdbus/dbus_proxy_async_interface_base.py b/src/sdbus/dbus_proxy_async_interface_base.py index 2dc8d92..2c29459 100644 --- a/src/sdbus/dbus_proxy_async_interface_base.py +++ b/src/sdbus/dbus_proxy_async_interface_base.py @@ -87,8 +87,6 @@ def __new__(cls, name: str, ) if isinstance(value, DbusSomethingAsync): - value.interface_name = interface_name - value.serving_enabled = serving_enabled dbus_declared_members[key] = value if isinstance(value, DbusMethodAsync): diff --git a/src/sdbus/dbus_proxy_async_method.py b/src/sdbus/dbus_proxy_async_method.py index 0c5214f..a889720 100644 --- a/src/sdbus/dbus_proxy_async_method.py +++ b/src/sdbus/dbus_proxy_async_method.py @@ -80,7 +80,6 @@ async def _call_dbus_async(self, *args: Any) -> Any: assert interface._attached_bus is not None assert interface._remote_service_name is not None assert interface._remote_object_path is not None - assert self.dbus_method.interface_name is not None new_call_message = interface._attached_bus. \ new_method_call_message( interface._remote_service_name, diff --git a/src/sdbus/dbus_proxy_async_property.py b/src/sdbus/dbus_proxy_async_property.py index 5928a2a..aa92dfd 100644 --- a/src/sdbus/dbus_proxy_async_property.py +++ b/src/sdbus/dbus_proxy_async_property.py @@ -130,7 +130,6 @@ async def get_async(self) -> T: assert interface._remote_service_name is not None assert interface._remote_object_path is not None assert self.dbus_property.property_name is not None - assert self.dbus_property.interface_name is not None new_call_message = interface._attached_bus. \ new_property_get_message( interface._remote_service_name, @@ -162,7 +161,6 @@ def _reply_set_sync(self, message: SdBusMessage) -> None: self.dbus_property.property_setter(interface, data_to_set_to) - assert self.dbus_property.interface_name is not None try: properties_changed = getattr(interface, 'properties_changed') except AttributeError: @@ -217,7 +215,6 @@ async def set_async(self, complete_object: T) -> None: assert interface._remote_service_name is not None assert interface._remote_object_path is not None assert self.dbus_property.property_name is not None - assert self.dbus_property.interface_name is not None new_call_message = interface._attached_bus. \ new_property_set_message( interface._remote_service_name, diff --git a/src/sdbus/dbus_proxy_async_signal.py b/src/sdbus/dbus_proxy_async_signal.py index 0c3bbc3..70d3a87 100644 --- a/src/sdbus/dbus_proxy_async_signal.py +++ b/src/sdbus/dbus_proxy_async_signal.py @@ -81,7 +81,6 @@ async def _get_dbus_queue(self) -> Queue[SdBusMessage]: assert interface._attached_bus is not None assert interface._remote_service_name is not None assert interface._remote_object_path is not None - assert self.dbus_signal.interface_name is not None assert self.dbus_signal.signal_name is not None return await interface._attached_bus.get_signal_queue_async( @@ -201,7 +200,6 @@ def _emit_message(self, args: T) -> None: assert interface._attached_bus is not None assert interface._serving_object_path is not None - assert self.dbus_signal.interface_name is not None assert self.dbus_signal.signal_name is not None signal_message = interface._attached_bus.new_signal_message( diff --git a/src/sdbus/dbus_proxy_sync_interface_base.py b/src/sdbus/dbus_proxy_sync_interface_base.py index 3624d5d..8379897 100644 --- a/src/sdbus/dbus_proxy_sync_interface_base.py +++ b/src/sdbus/dbus_proxy_sync_interface_base.py @@ -58,8 +58,6 @@ def __new__(cls, name: str, ) if isinstance(value, DbusSomethingSync): - value.interface_name = interface_name - value.serving_enabled = serving_enabled declared_interfaces.add(key) if isinstance(value, DbusMethodSync): diff --git a/src/sdbus/dbus_proxy_sync_method.py b/src/sdbus/dbus_proxy_sync_method.py index be0ab7f..6fa2165 100644 --- a/src/sdbus/dbus_proxy_sync_method.py +++ b/src/sdbus/dbus_proxy_sync_method.py @@ -55,7 +55,6 @@ def __init__(self, self.__doc__ = dbus_method.__doc__ def _call_dbus_sync(self, *args: Any) -> Any: - assert self.dbus_method.interface_name is not None new_call_message = self.interface._attached_bus. \ new_method_call_message( self.interface._remote_service_name, diff --git a/src/sdbus/dbus_proxy_sync_property.py b/src/sdbus/dbus_proxy_sync_property.py index 832f8ef..a16b5ae 100644 --- a/src/sdbus/dbus_proxy_sync_property.py +++ b/src/sdbus/dbus_proxy_sync_property.py @@ -69,7 +69,6 @@ def __get__(self, "This is probably an error as it will block " "other asyncio methods for considerable time." ) - assert self.interface_name is not None new_call_message = obj._attached_bus. \ new_property_get_message( @@ -97,7 +96,6 @@ def __set__(self, obj: DbusInterfaceBase, value: T) -> None: assert obj._remote_service_name is not None assert obj._remote_object_path is not None assert self.property_name is not None - assert self.interface_name is not None new_call_message = obj._attached_bus. \ new_property_set_message( obj._remote_service_name, diff --git a/test/test_sdbus_async_bad_class.py b/test/test_sdbus_async_bad_class.py index 5c3d60f..6b3c3d9 100644 --- a/test/test_sdbus_async_bad_class.py +++ b/test/test_sdbus_async_bad_class.py @@ -22,8 +22,6 @@ from unittest import TestCase from unittest import main as unittest_main -from sdbus.dbus_common_funcs import PROPERTY_FLAGS_MASK, count_bits - from sdbus import ( DbusDeprecatedFlag, DbusInterfaceCommonAsync, @@ -34,11 +32,15 @@ dbus_property_async, dbus_signal_async, ) +from sdbus.dbus_common_funcs import PROPERTY_FLAGS_MASK, count_bits from .common_test_util import skip_if_no_asserts, skip_if_no_name_validations -class TestInterface(DbusInterfaceCommonAsync): +class TestInterface( + DbusInterfaceCommonAsync, + interface_name="org.example.test", +): @dbus_method_async(result_signature="i") async def test_int(self) -> int: return 1 @@ -170,6 +172,22 @@ class TestInheritence2(TestInterface): async def test_unrelated(self) -> int: return 2 + def test_dbus_elements_without_interface_name(self) -> None: + with self.assertRaisesRegex(TypeError, "without interface name"): + + class NoInterfaceName(DbusInterfaceCommonAsync): + @dbus_method_async() + async def example(self) -> None: + ... + + def test_dbus_elements_without_interface_name_subclass(self) -> None: + with self.assertRaisesRegex(TypeError, "without interface name"): + + class NoInterfaceName(TestInterface): + @dbus_method_async() + async def example(self) -> None: + ... + if __name__ == "__main__": unittest_main() diff --git a/test/test_sdbus_block_bad_class.py b/test/test_sdbus_block_bad_class.py index 0df0a5d..076ad11 100644 --- a/test/test_sdbus_block_bad_class.py +++ b/test/test_sdbus_block_bad_class.py @@ -27,7 +27,10 @@ from .common_test_util import skip_if_no_name_validations -class GoodDbusInterface(DbusInterfaceCommon): +class GoodDbusInterface( + DbusInterfaceCommon, + interface_name="org.example.test", +): @dbus_method() def test_method(self) -> None: raise NotImplementedError @@ -118,6 +121,14 @@ class BadPropertyName( def test(self) -> str: return "test" + def test_dbus_elements_without_interface_name(self) -> None: + with self.assertRaisesRegex(TypeError, "without interface name"): + + class NoInterfaceName(DbusInterfaceCommon): + @dbus_method() + def example(self) -> None: + ... + if __name__ == "__main__": unittest_main() From 1066153f272fa5b58314dc3cfbcce39e7c7e35ce Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sat, 2 Dec 2023 20:25:25 +0600 Subject: [PATCH 053/188] Rework blocking interface internal metaclasses Instead of multiple different _private attributes use only two: * `_dbus`: contains current object remote metadata such as service name, object path and attached bus. * `_dbus_meta`: contains interface class metadata such as D-Bus member names to python attributes and the reverse. Async interface will be reworked next utilizing the same metadata classes. --- src/sdbus/dbus_common_elements.py | 26 ++++ src/sdbus/dbus_proxy_sync_interface_base.py | 129 ++++++++++++-------- src/sdbus/dbus_proxy_sync_interfaces.py | 8 +- src/sdbus/dbus_proxy_sync_method.py | 11 +- src/sdbus/dbus_proxy_sync_property.py | 27 ++-- test/test_sdbus_async_bad_class.py | 3 +- test/test_sdbus_block.py | 6 +- test/test_sdbus_block_bad_class.py | 16 +++ 8 files changed, 146 insertions(+), 80 deletions(-) diff --git a/src/sdbus/dbus_common_elements.py b/src/sdbus/dbus_common_elements.py index 45e1204..aa50e4a 100644 --- a/src/sdbus/dbus_common_elements.py +++ b/src/sdbus/dbus_common_elements.py @@ -25,6 +25,7 @@ from .dbus_common_funcs import ( _is_property_flags_correct, _method_name_converter, + get_default_bus, ) from .sd_bus_internals import is_interface_name_valid, is_member_name_valid @@ -37,12 +38,15 @@ List, Optional, Sequence, + Set, Tuple, TypeVar, ) T = TypeVar('T') + from .sd_bus_internals import SdBus + class DbusSomethingCommon: interface_name: str @@ -297,3 +301,25 @@ def __init__(self, original: T): def setter(self, new_setter: Optional[Callable[[Any, T], None]]) -> None: self.setter_overload = new_setter + + +class DbusRemoteObjectMeta: + def __init__( + self, + service_name: str, + object_path: str, + bus: Optional[SdBus] = None, + ): + self.service_name = service_name + self.object_path = object_path + self.attached_bus = ( + bus if bus is not None + else get_default_bus() + ) + + +class DbusClassMeta: + def __init__(self) -> None: + self.dbus_member_to_python_attr: Dict[str, str] = {} + self.dbus_interfaces_names: Set[str] = set() + self.python_attr_to_dbus_member: Dict[str, str] = {} diff --git a/src/sdbus/dbus_proxy_sync_interface_base.py b/src/sdbus/dbus_proxy_sync_interface_base.py index 8379897..02e5846 100644 --- a/src/sdbus/dbus_proxy_sync_interface_base.py +++ b/src/sdbus/dbus_proxy_sync_interface_base.py @@ -22,16 +22,17 @@ from typing import TYPE_CHECKING, cast from .dbus_common_elements import ( + DbusClassMeta, DbusInterfaceMetaCommon, + DbusRemoteObjectMeta, DbusSomethingAsync, - DbusSomethingSync, + DbusSomethingCommon, ) -from .dbus_common_funcs import get_default_bus from .dbus_proxy_sync_method import DbusMethodSync from .dbus_proxy_sync_property import DbusPropertySync if TYPE_CHECKING: - from typing import Any, Dict, Optional, Set, Tuple + from typing import Any, ClassVar, Dict, Optional, Tuple from .sd_bus_internals import SdBus @@ -44,46 +45,78 @@ def __new__(cls, name: str, serving_enabled: bool = True, ) -> DbusInterfaceMetaSync: - dbus_served_interfaces_names = ( - {interface_name} - if serving_enabled and interface_name is not None - else set() - ) - dbus_to_python_name_map: Dict[str, str] = {} - declared_interfaces = set() - # Set interface name - for key, value in namespace.items(): - assert not isinstance(value, DbusSomethingAsync), ( - "Can't mix async methods in sync interface." - ) - - if isinstance(value, DbusSomethingSync): - declared_interfaces.add(key) - - if isinstance(value, DbusMethodSync): - dbus_to_python_name_map[value.method_name] = key - elif isinstance(value, DbusPropertySync): - dbus_to_python_name_map[value.property_name] = key - - super_declared_interfaces = set() - for base in bases: - if issubclass(base, DbusInterfaceBase): - super_declared_interfaces.update( - base._dbus_declared_interfaces) + dbus_class_meta = DbusClassMeta() + if interface_name is not None: + dbus_class_meta.dbus_interfaces_names.add(interface_name) + + for attr_name, attr in namespace.items(): + if not isinstance(attr, DbusSomethingCommon): + continue + + if isinstance(attr, DbusSomethingAsync): + raise TypeError( + f"Can't mix async methods in sync interface: {attr_name!r}" + ) + + if isinstance(attr, DbusMethodSync): + dbus_class_meta.dbus_member_to_python_attr[ + attr.method_name] = attr_name + dbus_class_meta.python_attr_to_dbus_member[ + attr_name] = attr.method_name + elif isinstance(attr, DbusPropertySync): + dbus_class_meta.dbus_member_to_python_attr[ + attr.property_name] = attr_name + dbus_class_meta.python_attr_to_dbus_member[ + attr_name] = attr.property_name + else: + raise TypeError(f"Unknown D-Bus element: {attr!r}") - dbus_to_python_name_map.update( - base._dbus_to_python_name_map + for base in bases: + if not issubclass(base, DbusInterfaceBase): + continue + + # Update interfaces names set + base_interfaces_names = base._dbus_meta.dbus_interfaces_names + if dbus_interface_name_collision := ( + dbus_class_meta.dbus_interfaces_names + & base_interfaces_names + ): + raise TypeError( + f"Interface {name!r} and {base!r} have interface name " + f"collision: {dbus_interface_name_collision}" + ) + else: + dbus_class_meta.dbus_interfaces_names.update( + base_interfaces_names ) - for key in super_declared_interfaces & namespace.keys(): - raise TypeError("Attempted to overload D-Bus definition" - " blocking interfaces do not support overloading") + if dbus_member_collision := ( + dbus_class_meta.dbus_member_to_python_attr.keys() + & base._dbus_meta.dbus_member_to_python_attr.keys() + ): + raise TypeError( + f"Interface {name!r} and {base!r} have D-Bus member " + f"collision: {dbus_member_collision}" + ) + else: + dbus_class_meta.dbus_member_to_python_attr.update( + base._dbus_meta.dbus_member_to_python_attr + ) - namespace['_dbus_served_interfaces_names'] = \ - dbus_served_interfaces_names - namespace['_dbus_declared_interfaces'] = declared_interfaces - namespace['_dbus_to_python_name_map'] = dbus_to_python_name_map + if python_attr_collision := ( + namespace.keys() + & base._dbus_meta.python_attr_to_dbus_member.keys() + ): + raise TypeError( + f"Interface {name!r} and {base!r} have Python attribute " + f"collision: {python_attr_collision}" + ) + else: + dbus_class_meta.python_attr_to_dbus_member.update( + base._dbus_meta.python_attr_to_dbus_member + ) + namespace['_dbus_meta'] = dbus_class_meta new_cls = super().__new__( cls, name, bases, namespace, interface_name, @@ -94,18 +127,12 @@ def __new__(cls, name: str, class DbusInterfaceBase(metaclass=DbusInterfaceMetaSync): - _dbus_declared_interfaces: Set[str] - _dbus_serving_enabled: bool - _dbus_to_python_name_map: Dict[str, str] - _dbus_served_interfaces_names: Set[str] + _dbus_meta: ClassVar[DbusClassMeta] def __init__( - self, - service_name: str, - object_path: str, - bus: Optional[SdBus] = None, ) -> None: - self._remote_service_name = service_name - self._remote_object_path = object_path - self._attached_bus: SdBus = ( - bus if bus is not None - else get_default_bus()) + self, + service_name: str, + object_path: str, + bus: Optional[SdBus] = None, + ): + self._dbus = DbusRemoteObjectMeta(service_name, object_path, bus) diff --git a/src/sdbus/dbus_proxy_sync_interfaces.py b/src/sdbus/dbus_proxy_sync_interfaces.py index 0c72ff6..b0d88eb 100644 --- a/src/sdbus/dbus_proxy_sync_interfaces.py +++ b/src/sdbus/dbus_proxy_sync_interfaces.py @@ -70,12 +70,12 @@ def properties_get_all_dict( ) -> Dict[str, Any]: properties: Dict[str, Any] = {} - for interface_name in self._dbus_served_interfaces_names: - dbus_properties_data = self._properties_get_all( - interface_name) + for interface_name in self._dbus_meta.dbus_interfaces_names: + dbus_properties_data = self._properties_get_all(interface_name) for member_name, variant in dbus_properties_data.items(): try: - python_name = self._dbus_to_python_name_map[member_name] + python_name = self._dbus_meta.dbus_member_to_python_attr[ + member_name] except KeyError: if on_unknown_member == 'error': raise diff --git a/src/sdbus/dbus_proxy_sync_method.py b/src/sdbus/dbus_proxy_sync_method.py index 6fa2165..4d4c487 100644 --- a/src/sdbus/dbus_proxy_sync_method.py +++ b/src/sdbus/dbus_proxy_sync_method.py @@ -55,18 +55,19 @@ def __init__(self, self.__doc__ = dbus_method.__doc__ def _call_dbus_sync(self, *args: Any) -> Any: - new_call_message = self.interface._attached_bus. \ - new_method_call_message( - self.interface._remote_service_name, - self.interface._remote_object_path, + new_call_message = ( + self.interface._dbus.attached_bus.new_method_call_message( + self.interface._dbus.service_name, + self.interface._dbus.object_path, self.dbus_method.interface_name, self.dbus_method.method_name, ) + ) if args: new_call_message.append_data( self.dbus_method.input_signature, *args) - reply_message = self.interface._attached_bus.call( + reply_message = self.interface._dbus.attached_bus.call( new_call_message) return reply_message.get_contents() diff --git a/src/sdbus/dbus_proxy_sync_property.py b/src/sdbus/dbus_proxy_sync_property.py index a16b5ae..6f4f303 100644 --- a/src/sdbus/dbus_proxy_sync_property.py +++ b/src/sdbus/dbus_proxy_sync_property.py @@ -70,16 +70,16 @@ def __get__(self, "other asyncio methods for considerable time." ) - new_call_message = obj._attached_bus. \ - new_property_get_message( - obj._remote_service_name, - obj._remote_object_path, + new_call_message = ( + obj._dbus.attached_bus.new_property_get_message( + obj._dbus.service_name, + obj._dbus.object_path, self.interface_name, self.property_name, ) + ) - reply_message = obj._attached_bus. \ - call(new_call_message) + reply_message = obj._dbus.attached_bus.call(new_call_message) return cast(T, reply_message.get_contents()[1]) def __set__(self, obj: DbusInterfaceBase, value: T) -> None: @@ -92,22 +92,19 @@ def __set__(self, obj: DbusInterfaceBase, value: T) -> None: if not self.property_signature: raise AttributeError('D-Bus property is read only') - assert obj._attached_bus is not None - assert obj._remote_service_name is not None - assert obj._remote_object_path is not None - assert self.property_name is not None - new_call_message = obj._attached_bus. \ - new_property_set_message( - obj._remote_service_name, - obj._remote_object_path, + new_call_message = ( + obj._dbus.attached_bus.new_property_set_message( + obj._dbus.service_name, + obj._dbus.object_path, self.interface_name, self.property_name, ) + ) new_call_message.append_data( 'v', (self.property_signature, value)) - obj._attached_bus.call(new_call_message) + obj._dbus.attached_bus.call(new_call_message) def dbus_property( diff --git a/test/test_sdbus_async_bad_class.py b/test/test_sdbus_async_bad_class.py index 6b3c3d9..a8b345a 100644 --- a/test/test_sdbus_async_bad_class.py +++ b/test/test_sdbus_async_bad_class.py @@ -22,6 +22,8 @@ from unittest import TestCase from unittest import main as unittest_main +from sdbus.dbus_common_funcs import PROPERTY_FLAGS_MASK, count_bits + from sdbus import ( DbusDeprecatedFlag, DbusInterfaceCommonAsync, @@ -32,7 +34,6 @@ dbus_property_async, dbus_signal_async, ) -from sdbus.dbus_common_funcs import PROPERTY_FLAGS_MASK, count_bits from .common_test_util import skip_if_no_asserts, skip_if_no_name_validations diff --git a/test/test_sdbus_block.py b/test/test_sdbus_block.py index 1c25e50..a0d7f0f 100644 --- a/test/test_sdbus_block.py +++ b/test/test_sdbus_block.py @@ -45,17 +45,15 @@ def test_sync(self) -> None: self.assertIsInstance( s.get_connection_uid('org.freedesktop.DBus'), int) - def test_invalid_assignment() -> None: + with self.assertRaises(DbusPropertyReadOnlyError): s.features = ['test'] - self.assertRaises(DbusPropertyReadOnlyError, test_invalid_assignment) - self.assertTrue(s.get_name_owner('org.example.test')) with self.subTest('Test dbus to python name map'): self.assertEqual( 'features', - s._dbus_to_python_name_map['Features'], + s._dbus_meta.dbus_member_to_python_attr['Features'], ) with self.subTest('Test properties_get_all_dict'): diff --git a/test/test_sdbus_block_bad_class.py b/test/test_sdbus_block_bad_class.py index 076ad11..1b703c5 100644 --- a/test/test_sdbus_block_bad_class.py +++ b/test/test_sdbus_block_bad_class.py @@ -78,6 +78,22 @@ class GoodSubclass(GoodDbusInterface): def new_method(self) -> int: return 1 + def test_interface_collision(self) -> None: + with self.subTest("No collision"): + class NonInterface(GoodDbusInterface): + def do_work(self) -> None: + ... + + class NewExampleInterface( + DbusInterfaceCommon, + interface_name="org.example.test", + ): + ... + + with self.subTest("Collision"), self.assertRaises(TypeError): + class Collision(NewExampleInterface, GoodDbusInterface): + ... + def test_bad_class_names(self) -> None: skip_if_no_name_validations() From 0bd93c39f85ed1b5f22cb966e15eb0edf1ae720c Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sat, 16 Dec 2023 20:13:57 +0600 Subject: [PATCH 054/188] Rework async interface internal metaclasses Instead of multiple different _private attributes use only two: * `_dbus`: contains either remote objects metadata such as service name, object path and attached bus or local object metadata. * `_dbus_meta`: contains interface class metadata such as D-Bus member names to python attributes and the reverse. All descriptor protocols now return different classes based on if the D-Bus element was accessed from a proxy or local object. However, the API should remain the same. --- src/sdbus/autodoc.py | 18 +- src/sdbus/dbus_common_elements.py | 12 +- src/sdbus/dbus_proxy_async_interface_base.py | 265 ++++++++++------ src/sdbus/dbus_proxy_async_interfaces.py | 8 +- src/sdbus/dbus_proxy_async_method.py | 162 ++++++---- src/sdbus/dbus_proxy_async_property.py | 214 +++++++------ src/sdbus/dbus_proxy_async_signal.py | 318 +++++++++++-------- src/sdbus/utils.py | 10 +- test/test_low_level_errors.py | 11 +- test/test_sdbus_async.py | 85 +++-- 10 files changed, 653 insertions(+), 450 deletions(-) diff --git a/src/sdbus/autodoc.py b/src/sdbus/autodoc.py index 3e3b054..11fe41c 100644 --- a/src/sdbus/autodoc.py +++ b/src/sdbus/autodoc.py @@ -23,12 +23,12 @@ from sphinx.ext.autodoc import AttributeDocumenter, MethodDocumenter -from .dbus_proxy_async_method import DbusMethodAsyncBinded +from .dbus_proxy_async_method import DbusMethodAsyncClassBind from .dbus_proxy_async_property import ( DbusPropertyAsync, - DbusPropertyAsyncBinded, + DbusPropertyAsyncClassBind, ) -from .dbus_proxy_async_signal import DbusSignalAsync, DbusSignalBinded +from .dbus_proxy_async_signal import DbusSignalAsync, DbusSignalAsyncClassBind if TYPE_CHECKING: from typing import Any, Dict @@ -38,13 +38,13 @@ class DbusMethodDocumenter(MethodDocumenter): - objtype = 'DbusMethodAsyncBinded' + objtype = 'DbusMethodAsyncClassBind' directivetype = 'method' priority = 100 + MethodDocumenter.priority @classmethod def can_document_member(cls, member: Any, *args: Any) -> bool: - return isinstance(member, DbusMethodAsyncBinded) + return isinstance(member, DbusMethodAsyncClassBind) def import_object(self, raiseerror: bool = False) -> bool: self.objpath.append('dbus_method') @@ -68,13 +68,13 @@ def add_content(self, class DbusPropertyDocumenter(AttributeDocumenter): - objtype = 'DbusPropertyAsyncBinded' + objtype = 'DbusPropertyAsyncClassBind' directivetype = 'attribute' priority = 100 + AttributeDocumenter.priority @classmethod def can_document_member(cls, member: Any, *args: Any) -> bool: - return isinstance(member, DbusPropertyAsyncBinded) + return isinstance(member, DbusPropertyAsyncClassBind) def import_object(self, raiseerror: bool = False) -> bool: @@ -109,13 +109,13 @@ def add_content(self, class DbusSignalDocumenter(AttributeDocumenter): - objtype = 'DbusSignalBinded' + objtype = 'DbusSignalAsyncClassBind' directivetype = 'attribute' priority = 100 + AttributeDocumenter.priority @classmethod def can_document_member(cls, member: Any, *args: Any) -> bool: - return isinstance(member, DbusSignalBinded) + return isinstance(member, DbusSignalAsyncClassBind) def import_object(self, raiseerror: bool = False) -> bool: diff --git a/src/sdbus/dbus_common_elements.py b/src/sdbus/dbus_common_elements.py index aa50e4a..649b073 100644 --- a/src/sdbus/dbus_common_elements.py +++ b/src/sdbus/dbus_common_elements.py @@ -30,6 +30,7 @@ from .sd_bus_internals import is_interface_name_valid, is_member_name_valid if TYPE_CHECKING: + from asyncio import Queue from types import FunctionType from typing import ( Any, @@ -45,7 +46,7 @@ T = TypeVar('T') - from .sd_bus_internals import SdBus + from .sd_bus_internals import SdBus, SdBusInterface class DbusSomethingCommon: @@ -318,6 +319,15 @@ def __init__( ) +class DbusLocalObjectMeta: + def __init__(self) -> None: + self.activated_interfaces: List[SdBusInterface] = [] + self.serving_object_path: Optional[str] = None + self.attached_bus: Optional[SdBus] = None + self.local_signal_queues: Dict[ + Tuple[str, str], Set[Queue[Any]]] = {} + + class DbusClassMeta: def __init__(self) -> None: self.dbus_member_to_python_attr: Dict[str, str] = {} diff --git a/src/sdbus/dbus_proxy_async_interface_base.py b/src/sdbus/dbus_proxy_async_interface_base.py index 2c29459..542eab5 100644 --- a/src/sdbus/dbus_proxy_async_interface_base.py +++ b/src/sdbus/dbus_proxy_async_interface_base.py @@ -19,31 +19,48 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA from __future__ import annotations -from copy import deepcopy +from copy import copy from inspect import getmembers from types import MethodType from typing import TYPE_CHECKING, Any, Callable, cast from warnings import warn -from weakref import ref as weak_ref from .dbus_common_elements import ( + DbusClassMeta, DbusInterfaceMetaCommon, + DbusLocalObjectMeta, DbusOverload, + DbusRemoteObjectMeta, DbusSomethingAsync, + DbusSomethingCommon, DbusSomethingSync, ) from .dbus_common_funcs import get_default_bus -from .dbus_proxy_async_method import DbusMethodAsync, DbusMethodAsyncBinded +from .dbus_proxy_async_method import ( + DbusMethodAsync, + DbusMethodAsyncClassBind, + DbusMethodAsyncLocalBind, +) from .dbus_proxy_async_property import ( DbusPropertyAsync, - DbusPropertyAsyncBinded, + DbusPropertyAsyncClassBind, + DbusPropertyAsyncLocalBind, ) -from .dbus_proxy_async_signal import DbusSignalAsync, DbusSignalBinded +from .dbus_proxy_async_signal import DbusSignalAsync, DbusSignalAsyncLocalBind from .sd_bus_internals import SdBusInterface if TYPE_CHECKING: - from asyncio import Queue - from typing import Dict, List, Optional, Set, Tuple, Type, TypeVar + from typing import ( + ClassVar, + Dict, + List, + Optional, + Set, + Tuple, + Type, + TypeVar, + Union, + ) from .dbus_common_elements import DbusBindedAsync from .sd_bus_internals import SdBus @@ -59,85 +76,127 @@ def __new__(cls, name: str, serving_enabled: bool = True, ) -> DbusInterfaceMetaAsync: - dbus_served_interfaces_names = ( - {interface_name} - if serving_enabled and interface_name is not None - else set() - ) - dbus_to_python_name_map: Dict[str, str] = {} - dbus_declared_members: Dict[str, DbusSomethingAsync] = {} - superclass_members: Dict[str, DbusSomethingAsync] = {} + dbus_class_meta = DbusClassMeta() + if interface_name is not None and serving_enabled: + dbus_class_meta.dbus_interfaces_names.add(interface_name) - for base in bases: - if issubclass(base, DbusInterfaceBaseAsync): - dbus_to_python_name_map.update( - base._dbus_to_python_name_map - ) - dbus_served_interfaces_names.update( - base._dbus_served_interfaces_names - ) + overrides: Dict[str, DbusOverload] = {} + unresolved_collisions: Set[str] = set() + + for attr_name, attr in namespace.items(): + if isinstance(attr, DbusOverload): + overrides[attr_name] = attr + continue + + if not isinstance(attr, DbusSomethingCommon): + continue - superclass_members.update( - base._dbus_declared_members + if isinstance(attr, DbusSomethingSync): + raise TypeError( + "Can't mix blocking methods in " + f"async interface: {attr_name!r}" ) - for key, value in namespace.items(): - assert not isinstance(value, DbusSomethingSync), ( - "Can't mix sync methods in async interface." - ) + if isinstance(attr, DbusMethodAsync): + dbus_class_meta.dbus_member_to_python_attr[ + attr.method_name] = attr_name + dbus_class_meta.python_attr_to_dbus_member[ + attr_name] = attr.method_name + elif isinstance(attr, DbusPropertyAsync): + dbus_class_meta.dbus_member_to_python_attr[ + attr.property_name] = attr_name + dbus_class_meta.python_attr_to_dbus_member[ + attr_name] = attr.property_name + elif isinstance(attr, DbusSignalAsync): + dbus_class_meta.dbus_member_to_python_attr[ + attr.signal_name] = attr_name + dbus_class_meta.python_attr_to_dbus_member[ + attr_name] = attr.signal_name + else: + raise TypeError(f"Unknown D-Bus element: {attr!r}") - if isinstance(value, DbusSomethingAsync): - dbus_declared_members[key] = value + for base in bases: + if not issubclass(base, DbusInterfaceBaseAsync): + continue - if isinstance(value, DbusMethodAsync): - dbus_to_python_name_map[value.method_name] = key - elif isinstance(value, DbusPropertyAsync): - dbus_to_python_name_map[value.property_name] = key - elif isinstance(value, DbusSignalAsync): - dbus_to_python_name_map[value.signal_name] = key + # Update interfaces names set + base_interfaces_names = base._dbus_meta.dbus_interfaces_names + if dbus_interface_name_collision := ( + dbus_class_meta.dbus_interfaces_names + & base_interfaces_names + ): + raise TypeError( + f"Interface {name!r} and {base!r} have interface name " + f"collision: {dbus_interface_name_collision}" + ) + else: + dbus_class_meta.dbus_interfaces_names.update( + base_interfaces_names + ) - try: - super_dbus_def = superclass_members[key] - except KeyError: - if isinstance(value, DbusOverload): - raise TypeError( - f"No D-Bus member '{key}' to overload with." - ) + if dbus_member_collision := ( + dbus_class_meta.dbus_member_to_python_attr.keys() + & base._dbus_meta.dbus_member_to_python_attr.keys() + ): + raise TypeError( + f"Interface {name!r} and {base!r} have D-Bus member " + f"collision: {dbus_member_collision}" + ) else: - if not isinstance(value, DbusOverload): - raise TypeError( - "Attempted to overload D-Bus definition" - " without using @dbus_overload decorator" - ) + dbus_class_meta.dbus_member_to_python_attr.update( + base._dbus_meta.dbus_member_to_python_attr + ) - if isinstance(super_dbus_def, DbusMethodAsync): - new_method_def = deepcopy(super_dbus_def) - new_method_def.original_method = cast( - MethodType, value.original) + for collision_name in ( + namespace.keys() + & base._dbus_meta.python_attr_to_dbus_member.keys() + ): + try: + override = overrides.pop(collision_name) + except KeyError: + unresolved_collisions.add(collision_name) + continue - namespace[key] = new_method_def - elif isinstance(super_dbus_def, DbusPropertyAsync): - new_property_def = deepcopy(super_dbus_def) - new_property_def.property_getter = cast( + super_element = getattr(base, collision_name) + dbus_element_override: DbusSomethingAsync + if isinstance(super_element, DbusMethodAsyncClassBind): + dbus_element_override = copy(super_element.dbus_method) + dbus_element_override.original_method = cast( + MethodType, override.original) + elif isinstance(super_element, DbusPropertyAsyncClassBind): + dbus_element_override = copy(super_element.dbus_property) + dbus_element_override.property_getter = cast( Callable[[DbusInterfaceBaseAsync], Any], - value.original) - if value.setter_overload is not None: - new_property_def.property_setter = ( - value.setter_overload + override.original) + if override.setter_overload is not None: + dbus_element_override.property_setter = ( + override.setter_overload ) - - namespace[key] = new_property_def else: - raise TypeError('Unknown D-Bus overload') + raise TypeError( + f"Unknown override {collision_name!r} " + f"with {super_element!r}" + ) + + namespace[collision_name] = dbus_element_override - dbus_declared_members.update(superclass_members) + dbus_class_meta.python_attr_to_dbus_member.update( + base._dbus_meta.python_attr_to_dbus_member + ) + + if unresolved_collisions: + raise TypeError( + f"Interface {name!r} and {base!r} have Python attribute " + f"collision: {unresolved_collisions}" + ) + + if overrides: + raise TypeError( + f"Interface {name!r} has unresolved overrides:", + set(overrides.keys()), + ) - namespace['_dbus_served_interfaces_names'] = \ - dbus_served_interfaces_names - namespace['_dbus_to_python_name_map'] = dbus_to_python_name_map - namespace['_dbus_interface_name'] = interface_name - namespace['_dbus_serving_enabled'] = serving_enabled - namespace['_dbus_declared_members'] = dbus_declared_members + namespace['_dbus_meta'] = dbus_class_meta new_cls = super().__new__( cls, name, bases, namespace, interface_name, @@ -148,21 +207,11 @@ def __new__(cls, name: str, class DbusInterfaceBaseAsync(metaclass=DbusInterfaceMetaAsync): - _dbus_interface_name: Optional[str] - _dbus_serving_enabled: bool - _dbus_to_python_name_map: Dict[str, str] - _dbus_served_interfaces_names: Set[str] - _dbus_declared_members: Dict[str, DbusSomethingAsync] + _dbus_meta: ClassVar[DbusClassMeta] def __init__(self) -> None: - self._activated_interfaces: List[SdBusInterface] = [] - self._is_binded: bool = False - self._remote_service_name: Optional[str] = None - self._remote_object_path: Optional[str] = None - self._attached_bus: Optional[SdBus] = None - self._serving_object_path: Optional[str] = None - self._local_signal_queues: \ - Dict[DbusSignalAsync[Any], List[weak_ref[Queue[Any]]]] = {} + self._dbus: Union[ + DbusRemoteObjectMeta, DbusLocalObjectMeta] = DbusLocalObjectMeta() async def start_serving(self, object_path: str, @@ -179,34 +228,43 @@ def export_to_dbus( bus: Optional[SdBus] = None, ) -> None: + local_object_meta = self._dbus + if isinstance(local_object_meta, DbusRemoteObjectMeta): + raise RuntimeError("Cannot export D-Bus proxies.") + + # TODO: Being able to serve multiple buses and object + if local_object_meta.attached_bus is not None: + raise RuntimeError( + "Object already exported. " + "This limitation should be fixed in future version." + ) + if bus is None: bus = get_default_bus() - # TODO: Being able to serve multiple buses and object - self._attached_bus = bus - self._serving_object_path = object_path + + local_object_meta.attached_bus = bus + local_object_meta.serving_object_path = object_path # TODO: can be optimized with a single loop interface_map: Dict[str, List[DbusBindedAsync]] = {} for key, value in getmembers(self): assert not isinstance(value, DbusSomethingAsync) - if isinstance(value, DbusMethodAsyncBinded): + if isinstance(value, DbusMethodAsyncLocalBind): interface_name = value.dbus_method.interface_name if not value.dbus_method.serving_enabled: continue - elif isinstance(value, DbusPropertyAsyncBinded): + elif isinstance(value, DbusPropertyAsyncLocalBind): interface_name = value.dbus_property.interface_name if not value.dbus_property.serving_enabled: continue - elif isinstance(value, DbusSignalBinded): + elif isinstance(value, DbusSignalAsyncLocalBind): interface_name = value.dbus_signal.interface_name if not value.dbus_signal.serving_enabled: continue else: continue - assert interface_name is not None - try: interface_member_list = interface_map[interface_name] except KeyError: @@ -218,7 +276,7 @@ def export_to_dbus( for interface_name, member_list in interface_map.items(): new_interface = SdBusInterface() for dbus_something in member_list: - if isinstance(dbus_something, DbusMethodAsyncBinded): + if isinstance(dbus_something, DbusMethodAsyncLocalBind): new_interface.add_method( dbus_something.dbus_method.method_name, dbus_something.dbus_method.input_signature, @@ -226,10 +284,10 @@ def export_to_dbus( dbus_something.dbus_method.result_signature, dbus_something.dbus_method.result_args_names, dbus_something.dbus_method.flags, - dbus_something._call_from_dbus, + dbus_something._dbus_reply_call, ) - elif isinstance(dbus_something, DbusPropertyAsyncBinded): - getter = dbus_something._reply_get_sync + elif isinstance(dbus_something, DbusPropertyAsyncLocalBind): + getter = dbus_something._dbus_reply_get dbus_property = dbus_something.dbus_property if ( @@ -237,7 +295,7 @@ def export_to_dbus( and dbus_property.property_setter_is_public ): - setter = dbus_something._reply_set_sync + setter = dbus_something._dbus_reply_set else: setter = None @@ -248,7 +306,7 @@ def export_to_dbus( setter, dbus_property.flags, ) - elif isinstance(dbus_something, DbusSignalBinded): + elif isinstance(dbus_something, DbusSignalAsyncLocalBind): new_interface.add_signal( dbus_something.dbus_signal.signal_name, dbus_something.dbus_signal.signal_signature, @@ -260,7 +318,7 @@ def export_to_dbus( bus.add_interface(new_interface, object_path, interface_name) - self._activated_interfaces.append(new_interface) + local_object_meta.activated_interfaces.append(new_interface) def _connect( self, @@ -281,10 +339,11 @@ def _proxify( bus: Optional[SdBus] = None, ) -> None: - self._is_binded = True - self._attached_bus = bus if bus is not None else get_default_bus() - self._remote_service_name = service_name - self._remote_object_path = object_path + self._dbus = DbusRemoteObjectMeta( + service_name, + object_path, + bus, + ) @classmethod def new_connect( diff --git a/src/sdbus/dbus_proxy_async_interfaces.py b/src/sdbus/dbus_proxy_async_interfaces.py index 607f855..6b1b78b 100644 --- a/src/sdbus/dbus_proxy_async_interfaces.py +++ b/src/sdbus/dbus_proxy_async_interfaces.py @@ -88,13 +88,13 @@ async def properties_get_all_dict( properties: Dict[str, Any] = {} - for interface_name in self._dbus_served_interfaces_names: + for interface_name in self._dbus_meta.dbus_interfaces_names: dbus_properties_data = await self._properties_get_all( interface_name) properties.update( _parse_properties_vardict( - self._dbus_to_python_name_map, + self._dbus_meta.dbus_member_to_python_attr, dbus_properties_data, on_unknown_member, ) @@ -169,8 +169,8 @@ def export_with_manager( def remove_managed_object( self, managed_object: DbusInterfaceBaseAsync) -> None: - if self._attached_bus is None: + if self._dbus.attached_bus is None: raise RuntimeError('Object manager not exported') removed_path = self._managed_object_to_path.pop(managed_object) - self._attached_bus.emit_object_removed(removed_path) + self._dbus.attached_bus.emit_object_removed(removed_path) diff --git a/src/sdbus/dbus_proxy_async_method.py b/src/sdbus/dbus_proxy_async_method.py index a889720..3d7a900 100644 --- a/src/sdbus/dbus_proxy_async_method.py +++ b/src/sdbus/dbus_proxy_async_method.py @@ -1,6 +1,6 @@ # SPDX-License-Identifier: LGPL-2.1-or-later -# Copyright (C) 2020-2022 igo95862 +# Copyright (C) 2020-2023 igo95862 # This file is part of python-sdbus @@ -29,6 +29,7 @@ DbusBindedAsync, DbusMethodCommon, DbusOverload, + DbusRemoteObjectMeta, DbusSomethingAsync, ) from .dbus_exceptions import DbusFailedError @@ -53,85 +54,106 @@ def get_current_message() -> SdBusMessage: class DbusMethodAsync(DbusMethodCommon, DbusSomethingAsync): def __get__(self, - obj: DbusInterfaceBaseAsync, + obj: Optional[DbusInterfaceBaseAsync], obj_class: Optional[Type[DbusInterfaceBaseAsync]] = None, ) -> Callable[..., Any]: - return DbusMethodAsyncBinded(self, obj) + if obj is not None: + dbus_meta = obj._dbus + if isinstance(dbus_meta, DbusRemoteObjectMeta): + return DbusMethodAsyncProxyBind(self, dbus_meta) + else: + return DbusMethodAsyncLocalBind(self, obj) + else: + return DbusMethodAsyncClassBind(self) + + +class DbusMethodAsyncBaseBind(DbusBindedAsync): + + def __call__(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError -class DbusMethodAsyncBinded(DbusBindedAsync): - def __init__(self, - dbus_method: DbusMethodAsync, - interface: DbusInterfaceBaseAsync): +class DbusMethodAsyncProxyBind(DbusMethodAsyncBaseBind): + def __init__( + self, + dbus_method: DbusMethodAsync, + proxy_meta: DbusRemoteObjectMeta, + ): self.dbus_method = dbus_method - self.interface_ref = ( - weak_ref(interface) - if interface is not None - else None - ) + self.proxy_meta = proxy_meta self.__doc__ = dbus_method.__doc__ - async def _call_dbus_async(self, *args: Any) -> Any: - assert self.interface_ref is not None - interface = self.interface_ref() - assert interface is not None - - assert interface._attached_bus is not None - assert interface._remote_service_name is not None - assert interface._remote_object_path is not None - new_call_message = interface._attached_bus. \ - new_method_call_message( - interface._remote_service_name, - interface._remote_object_path, - self.dbus_method.interface_name, - self.dbus_method.method_name, - ) + async def _dbus_async_call(self, call_message: SdBusMessage) -> Any: + bus = self.proxy_meta.attached_bus + reply_message = await bus.call_async(call_message) + return reply_message.get_contents() + + @staticmethod + async def _no_reply() -> None: + return None + + def __call__(self, *args: Any, **kwargs: Any) -> Any: + bus = self.proxy_meta.attached_bus + dbus_method = self.dbus_method + + new_call_message = bus.new_method_call_message( + self.proxy_meta.service_name, + self.proxy_meta.object_path, + dbus_method.interface_name, + dbus_method.method_name, + ) + + if len(args) == dbus_method.num_of_args: + assert not kwargs, ( + "Passed more arguments than method supports" + f"Extra args: {kwargs}") + rebuilt_args: Sequence[Any] = args + else: + rebuilt_args = dbus_method._rebuild_args( + dbus_method.original_method, + *args, + **kwargs) - if args: + if rebuilt_args: new_call_message.append_data( - self.dbus_method.input_signature, *args) + dbus_method.input_signature, *rebuilt_args) - if self.dbus_method.flags & DbusNoReplyFlag: + if dbus_method.flags & DbusNoReplyFlag: new_call_message.expect_reply = False new_call_message.send() - return + return self._no_reply() - reply_message = await interface._attached_bus.call_async( - new_call_message) - return reply_message.get_contents() + return self._dbus_async_call(new_call_message) - def __call__(self, *args: Any, **kwargs: Any) -> Any: - assert self.interface_ref is not None - interface = self.interface_ref() - assert interface is not None - if interface._is_binded: +class DbusMethodAsyncLocalBind(DbusMethodAsyncBaseBind): + def __init__( + self, + dbus_method: DbusMethodAsync, + local_object: DbusInterfaceBaseAsync, + ): + self.dbus_method = dbus_method + self.local_object_ref = weak_ref(local_object) - if len(args) == self.dbus_method.num_of_args: - assert not kwargs, ( - "Passed more arguments than method supports" - f"Extra args: {kwargs}") - rebuilt_args: Sequence[Any] = args - else: - rebuilt_args = self.dbus_method._rebuild_args( - self.dbus_method.original_method, - *args, - **kwargs) + self.__doc__ = dbus_method.__doc__ - return self._call_dbus_async(*rebuilt_args) - else: - return self.dbus_method.original_method( - interface, *args, **kwargs) + def __call__(self, *args: Any, **kwargs: Any) -> Any: + local_object = self.local_object_ref() + if local_object is None: + raise RuntimeError("Local object no longer exists!") + + return self.dbus_method.original_method(local_object, *args, **kwargs) - async def _call_method_from_dbus( - self, - request_message: SdBusMessage, - interface: DbusInterfaceBaseAsync) -> Any: + async def _dbus_reply_call_method( + self, + request_message: SdBusMessage, + local_object: DbusInterfaceBaseAsync, + ) -> Any: request_data = request_message.get_contents() local_method = self.dbus_method.original_method.__get__( - interface, None) + local_object, None) CURRENT_MESSAGE.set(request_message) @@ -142,20 +164,21 @@ async def _call_method_from_dbus( else: return await local_method(request_data) - async def _call_from_dbus( - self, - request_message: SdBusMessage) -> None: - assert self.interface_ref is not None - interface = self.interface_ref() - assert interface is not None + async def _dbus_reply_call( + self, + request_message: SdBusMessage + ) -> None: + local_object = self.local_object_ref() + if local_object is None: + raise RuntimeError("Local object no longer exists!") call_context = copy_context() try: reply_data = await call_context.run( - self._call_method_from_dbus, + self._dbus_reply_call_method, request_message, - interface, + local_object, ) except DbusFailedError as e: if not request_message.expect_reply: @@ -197,6 +220,13 @@ async def _call_from_dbus( reply_message.send() +class DbusMethodAsyncClassBind(DbusMethodAsyncBaseBind): + def __init__(self, dbus_method: DbusMethodAsync): + self.dbus_method = dbus_method + + self.__doc__ = dbus_method.__doc__ + + def dbus_method_async( input_signature: str = "", result_signature: str = "", diff --git a/src/sdbus/dbus_proxy_async_property.py b/src/sdbus/dbus_proxy_async_property.py index aa92dfd..b5139aa 100644 --- a/src/sdbus/dbus_proxy_async_property.py +++ b/src/sdbus/dbus_proxy_async_property.py @@ -1,6 +1,6 @@ # SPDX-License-Identifier: LGPL-2.1-or-later -# Copyright (C) 2020-2022 igo95862 +# Copyright (C) 2020-2023 igo95862 # This file is part of python-sdbus @@ -21,13 +21,14 @@ from inspect import iscoroutinefunction from types import FunctionType -from typing import TYPE_CHECKING, Generic, TypeVar, cast +from typing import TYPE_CHECKING, Awaitable, Generic, TypeVar, cast from weakref import ref as weak_ref from .dbus_common_elements import ( DbusBindedAsync, DbusOverload, DbusPropertyCommon, + DbusRemoteObjectMeta, DbusSomethingAsync, ) @@ -71,10 +72,17 @@ def __init__( self.__doc__ = property_getter.__doc__ def __get__(self, - obj: DbusInterfaceBaseAsync, + obj: Optional[DbusInterfaceBaseAsync], obj_class: Optional[Type[DbusInterfaceBaseAsync]] = None, - ) -> DbusPropertyAsyncBinded: - return DbusPropertyAsyncBinded(self, obj) + ) -> DbusPropertyAsyncBaseBind[T]: + if obj is not None: + dbus_meta = obj._dbus + if isinstance(dbus_meta, DbusRemoteObjectMeta): + return DbusPropertyAsyncProxyBind(self, dbus_meta) + else: + return DbusPropertyAsyncLocalBind(self, obj) + else: + return DbusPropertyAsyncClassBind(self) def setter(self, new_set_function: Callable[ @@ -101,68 +109,95 @@ def setter_private( self.property_setter_is_public = False -class DbusPropertyAsyncBinded(DbusBindedAsync): - def __init__(self, - dbus_property: DbusPropertyAsync[T], - interface: DbusInterfaceBaseAsync): +class DbusPropertyAsyncBaseBind(DbusBindedAsync, Awaitable[T]): + def __await__(self) -> Generator[Any, None, T]: + return self.get_async().__await__() + + async def get_async(self) -> T: + raise NotImplementedError + + async def set_async(self, complete_object: T) -> None: + raise NotImplementedError + + +class DbusPropertyAsyncProxyBind(DbusPropertyAsyncBaseBind[T]): + def __init__( + self, + dbus_property: DbusPropertyAsync[T], + proxy_meta: DbusRemoteObjectMeta, + ): self.dbus_property = dbus_property - self.interface_ref = ( - weak_ref(interface) - if interface is not None - else None - ) + self.proxy_meta = proxy_meta self.__doc__ = dbus_property.__doc__ - def __await__(self) -> Generator[Any, None, T]: - return self.get_async().__await__() - async def get_async(self) -> T: - assert self.interface_ref is not None - interface = self.interface_ref() - assert interface is not None - - if not interface._is_binded: - return self.dbus_property.property_getter( - interface) - - assert interface._attached_bus is not None - assert interface._remote_service_name is not None - assert interface._remote_object_path is not None - assert self.dbus_property.property_name is not None - new_call_message = interface._attached_bus. \ - new_property_get_message( - interface._remote_service_name, - interface._remote_object_path, + bus = self.proxy_meta.attached_bus + new_get_message = ( + bus.new_property_get_message( + self.proxy_meta.service_name, + self.proxy_meta.object_path, self.dbus_property.interface_name, self.dbus_property.property_name, ) - - reply_message = await interface._attached_bus. \ - call_async(new_call_message) + ) + reply_message = await bus.call_async(new_get_message) # Get method returns variant but we only need contents of variant return cast(T, reply_message.get_contents()[1]) - def _reply_get_sync(self, message: SdBusMessage) -> None: - assert self.interface_ref is not None - interface = self.interface_ref() - assert interface is not None + async def set_async(self, complete_object: T) -> None: + bus = self.proxy_meta.attached_bus + new_set_message = ( + bus.new_property_set_message( + self.proxy_meta.service_name, + self.proxy_meta.object_path, + self.dbus_property.interface_name, + self.dbus_property.property_name, + ) + ) + new_set_message.append_data( + 'v', + (self.dbus_property.property_signature, complete_object), + ) + await bus.call_async(new_set_message) - reply_data: Any = self.dbus_property.property_getter(interface) - message.append_data(self.dbus_property.property_signature, reply_data) - def _reply_set_sync(self, message: SdBusMessage) -> None: - assert self.interface_ref is not None - interface = self.interface_ref() - assert interface is not None +class DbusPropertyAsyncLocalBind(DbusPropertyAsyncBaseBind[T]): + def __init__( + self, + dbus_property: DbusPropertyAsync[T], + local_object: DbusInterfaceBaseAsync, + ): + self.dbus_property = dbus_property + self.local_object_ref = weak_ref(local_object) - assert self.dbus_property.property_setter is not None - data_to_set_to: Any = message.get_contents() + self.__doc__ = dbus_property.__doc__ + + async def get_async(self) -> T: + local_object = self.local_object_ref() + if local_object is None: + raise RuntimeError("Local object no longer exists!") - self.dbus_property.property_setter(interface, data_to_set_to) + return self.dbus_property.property_getter(local_object) + + async def set_async(self, complete_object: T) -> None: + if self.dbus_property.property_setter is None: + raise RuntimeError("Property has no setter") + + local_object = self.local_object_ref() + if local_object is None: + raise RuntimeError("Local object no longer exists!") + + self.dbus_property.property_setter( + local_object, + complete_object, + ) try: - properties_changed = getattr(interface, 'properties_changed') + properties_changed = getattr( + local_object, + "properties_changed", + ) except AttributeError: ... else: @@ -172,61 +207,58 @@ def _reply_set_sync(self, message: SdBusMessage) -> None: { self.dbus_property.property_name: ( self.dbus_property.property_signature, - data_to_set_to, + complete_object, ), }, [] ) ) - async def set_async(self, complete_object: T) -> None: - assert self.interface_ref is not None - interface = self.interface_ref() - assert interface is not None + def _dbus_reply_get(self, message: SdBusMessage) -> None: + local_object = self.local_object_ref() + if local_object is None: + raise RuntimeError("Local object no longer exists!") - if not interface._is_binded: - if self.dbus_property.property_setter is None: - raise ValueError('Property has no setter') + reply_data: Any = self.dbus_property.property_getter(local_object) + message.append_data(self.dbus_property.property_signature, reply_data) - self.dbus_property.property_setter( - interface, complete_object) + def _dbus_reply_set(self, message: SdBusMessage) -> None: + local_object = self.local_object_ref() + if local_object is None: + raise RuntimeError("Local object no longer exists!") - try: - properties_changed = getattr(interface, 'properties_changed') - except AttributeError: - ... - else: - properties_changed.emit( - ( - self.dbus_property.interface_name, - { - self.dbus_property.property_name: ( - self.dbus_property.property_signature, - complete_object, - ), - }, - [] - ) - ) + assert self.dbus_property.property_setter is not None + data_to_set_to: Any = message.get_contents() - return + self.dbus_property.property_setter(local_object, data_to_set_to) - assert interface._attached_bus is not None - assert interface._remote_service_name is not None - assert interface._remote_object_path is not None - assert self.dbus_property.property_name is not None - new_call_message = interface._attached_bus. \ - new_property_set_message( - interface._remote_service_name, - interface._remote_object_path, - self.dbus_property.interface_name, - self.dbus_property.property_name, + try: + properties_changed = getattr( + local_object, + "properties_changed", ) + except AttributeError: + ... + else: + properties_changed.emit( + ( + self.dbus_property.interface_name, + { + self.dbus_property.property_name: ( + self.dbus_property.property_signature, + data_to_set_to, + ), + }, + [] + ) + ) + - new_call_message.append_data( - 'v', (self.dbus_property.property_signature, complete_object)) +class DbusPropertyAsyncClassBind(DbusPropertyAsyncBaseBind[T]): + def __init__(self, dbus_property: DbusPropertyAsync[T]): + self.dbus_property = dbus_property - await interface._attached_bus.call_async(new_call_message) + self.__doc__ = dbus_property.__doc__ def dbus_property_async( diff --git a/src/sdbus/dbus_proxy_async_signal.py b/src/sdbus/dbus_proxy_async_signal.py index 70d3a87..369d9a1 100644 --- a/src/sdbus/dbus_proxy_async_signal.py +++ b/src/sdbus/dbus_proxy_async_signal.py @@ -1,6 +1,6 @@ # SPDX-License-Identifier: LGPL-2.1-or-later -# Copyright (C) 2020-2022 igo95862 +# Copyright (C) 2020-2023 igo95862 # This file is part of python-sdbus @@ -21,29 +21,29 @@ from asyncio import Queue from types import FunctionType -from typing import TYPE_CHECKING, Generic, TypeVar, cast -from weakref import ref as weak_ref +from typing import ( + TYPE_CHECKING, + AsyncIterable, + AsyncIterator, + Generic, + TypeVar, + cast, +) from .dbus_common_elements import ( DbusBindedAsync, + DbusLocalObjectMeta, + DbusRemoteObjectMeta, DbusSingalCommon, DbusSomethingAsync, ) from .dbus_common_funcs import get_default_bus if TYPE_CHECKING: - from typing import ( - Any, - AsyncGenerator, - Callable, - Optional, - Sequence, - Tuple, - Type, - ) + from typing import Any, Callable, Optional, Sequence, Tuple, Type from .dbus_proxy_async_interface_base import DbusInterfaceBaseAsync - from .sd_bus_internals import SdBus, SdBusMessage + from .sd_bus_internals import SdBus T = TypeVar('T') @@ -51,96 +51,62 @@ class DbusSignalAsync(DbusSomethingAsync, DbusSingalCommon, Generic[T]): - def __get__(self, - obj: Optional[DbusInterfaceBaseAsync], - obj_class: Optional[Type[DbusInterfaceBaseAsync]] = None, - ) -> DbusSignalBinded[T]: - return DbusSignalBinded(self, obj) - - -class DbusSignalBinded(Generic[T], DbusBindedAsync): - def __init__(self, - dbus_signal: DbusSignalAsync[T], - interface: Optional[DbusInterfaceBaseAsync]): - self.dbus_signal = dbus_signal - self.interface_ref = ( - weak_ref(interface) - if interface is not None - else None - ) - - self.__doc__ = dbus_signal.__doc__ + def __get__( + self, + obj: Optional[DbusInterfaceBaseAsync], + obj_class: Optional[Type[DbusInterfaceBaseAsync]] = None, + ) -> DbusSignalAsyncBaseBind[T]: + if obj is not None: + dbus_meta = obj._dbus + if isinstance(dbus_meta, DbusRemoteObjectMeta): + return DbusSignalAsyncProxyBind(self, dbus_meta) + else: + return DbusSignalAsyncLocalBind(self, dbus_meta) + else: + return DbusSignalAsyncClassBind(self) - async def _get_dbus_queue(self) -> Queue[SdBusMessage]: - assert self.interface_ref is not None, ( - "Called method from class?" - ) - interface = self.interface_ref() - assert interface is not None - assert interface._attached_bus is not None - assert interface._remote_service_name is not None - assert interface._remote_object_path is not None - assert self.dbus_signal.signal_name is not None +class DbusSignalAsyncBaseBind(DbusBindedAsync, AsyncIterable[T], Generic[T]): + async def catch(self) -> AsyncIterator[T]: + raise NotImplementedError + yield cast(T, None) - return await interface._attached_bus.get_signal_queue_async( - interface._remote_service_name, - interface._remote_object_path, - self.dbus_signal.interface_name, - self.dbus_signal.signal_name, - ) + __aiter__ = catch - def _cleanup_local_queue( + async def catch_anywhere( self, - queue_ref: weak_ref[Queue[T]]) -> None: - assert self.interface_ref is not None, ( - "Called method from class?" - ) - interface = self.interface_ref() - assert interface is not None - - interface._local_signal_queues[self.dbus_signal].remove(queue_ref) - - def _get_local_queue(self) -> Queue[T]: - assert self.interface_ref is not None, ( - "Called method from class?" - ) - interface = self.interface_ref() - assert interface is not None + service_name: Optional[str] = None, + bus: Optional[SdBus] = None, + ) -> AsyncIterable[Tuple[str, T]]: + raise NotImplementedError + yield "", cast(T, None) - try: - list_of_queues = interface._local_signal_queues[ - self.dbus_signal] - except KeyError: - list_of_queues = [] - interface._local_signal_queues[ - self.dbus_signal] = list_of_queues + def emit(self, args: T) -> None: + raise NotImplementedError - new_queue: Queue[T] = Queue() - list_of_queues.append(weak_ref(new_queue, self._cleanup_local_queue)) +class DbusSignalAsyncProxyBind(DbusSignalAsyncBaseBind[T]): + def __init__( + self, + dbus_signal: DbusSignalAsync[T], + proxy_meta: DbusRemoteObjectMeta, + ): + self.dbus_signal = dbus_signal + self.proxy_meta = proxy_meta - return new_queue + self.__doc__ = dbus_signal.__doc__ - async def catch(self) -> AsyncGenerator[T, None]: - assert self.interface_ref is not None, ( - "Called method from class?" + async def catch(self) -> AsyncIterator[T]: + dbus_queue = await self.proxy_meta.attached_bus.get_signal_queue_async( + self.proxy_meta.service_name, + self.proxy_meta.object_path, + self.dbus_signal.interface_name, + self.dbus_signal.signal_name, ) - interface = self.interface_ref() - assert interface is not None - - if interface._is_binded: - message_queue = await self._get_dbus_queue() - - while True: - next_signal_message = await message_queue.get() - yield cast(T, next_signal_message.get_contents()) - else: - data_queue = self._get_local_queue() - while True: - next_data = await data_queue.get() - yield next_data + while True: + next_signal_message = await dbus_queue.get() + yield cast(T, next_signal_message.get_contents()) __aiter__ = catch @@ -148,32 +114,12 @@ async def catch_anywhere( self, service_name: Optional[str] = None, bus: Optional[SdBus] = None, - ) -> AsyncGenerator[Tuple[str, T], None]: - if service_name is None: - if self.interface_ref is not None: - interface = self.interface_ref() - assert interface is not None - if interface._remote_service_name is None: - raise NotImplementedError( - 'catch_anywhere not implemented for ' - 'local objects' - ) - - service_name = interface._remote_service_name - else: - raise ValueError( - 'Called catch_anywhere from class ' - 'but service name was not provided' - ) - + ) -> AsyncIterable[Tuple[str, T]]: if bus is None: - if self.interface_ref is not None: - interface = self.interface_ref() - assert interface is not None - assert interface._attached_bus is not None - bus = interface._attached_bus - else: - bus = get_default_bus() + bus = self.proxy_meta.attached_bus + + if service_name is None: + service_name = self.proxy_meta.service_name message_queue = await bus.get_signal_queue_async( service_name, @@ -191,19 +137,67 @@ async def catch_anywhere( cast(T, next_signal_message.get_contents()) ) - def _emit_message(self, args: T) -> None: - assert self.interface_ref is not None, ( - "Called method from class?" + def emit(self, args: T) -> None: + raise RuntimeError("Cannot emit signal from D-Bus proxy.") + + +class DbusSignalAsyncLocalBind(DbusSignalAsyncBaseBind[T]): + def __init__( + self, + dbus_signal: DbusSignalAsync[T], + local_meta: DbusLocalObjectMeta, + ): + self.dbus_signal = dbus_signal + self.local_meta = local_meta + + self.__doc__ = dbus_signal.__doc__ + + async def catch(self) -> AsyncIterator[T]: + signal_key = ( + self.dbus_signal.interface_name, + self.dbus_signal.signal_name, ) - interface = self.interface_ref() - assert interface is not None - assert interface._attached_bus is not None - assert interface._serving_object_path is not None - assert self.dbus_signal.signal_name is not None + try: + list_of_queues = self.local_meta.local_signal_queues[ + signal_key + ] + except KeyError: + list_of_queues = set() + self.local_meta.local_signal_queues[ + signal_key] = list_of_queues + + new_queue: Queue[T] = Queue() + + list_of_queues.add(new_queue) + try: + while True: + next_data = await new_queue.get() + yield next_data + finally: + list_of_queues.remove(new_queue) + + __aiter__ = catch + + async def catch_anywhere( + self, + service_name: Optional[str] = None, + bus: Optional[SdBus] = None, + ) -> AsyncIterable[Tuple[str, T]]: + raise NotImplementedError("TODO") + yield + + def _emit_dbus_signal(self, args: T) -> None: + attached_bus = self.local_meta.attached_bus + if attached_bus is None: + return - signal_message = interface._attached_bus.new_signal_message( - interface._serving_object_path, + serving_object_path = self.local_meta.serving_object_path + if serving_object_path is None: + return + + signal_message = attached_bus.new_signal_message( + serving_object_path, self.dbus_signal.interface_name, self.dbus_signal.signal_name, ) @@ -222,26 +216,76 @@ def _emit_message(self, args: T) -> None: signal_message.send() def emit(self, args: T) -> None: - assert self.interface_ref is not None, ( - "Called method from class?" - ) - interface = self.interface_ref() - assert interface is not None + self._emit_dbus_signal(args) - if interface._activated_interfaces: - self._emit_message(args) + signal_key = ( + self.dbus_signal.interface_name, + self.dbus_signal.signal_name, + ) try: - list_of_queues = interface._local_signal_queues[self.dbus_signal] + list_of_queues = self.local_meta.local_signal_queues[ + signal_key] except KeyError: return - for local_queue_ref in list_of_queues: - local_queue = local_queue_ref() - assert local_queue is not None + for local_queue in list_of_queues: local_queue.put_nowait(args) +class DbusSignalAsyncClassBind(DbusSignalAsyncBaseBind[T]): + def __init__( + self, + dbus_signal: DbusSignalAsync[T], + ): + self.dbus_signal = dbus_signal + + self.__doc__ = dbus_signal.__doc__ + + async def catch(self) -> AsyncIterator[T]: + raise NotImplementedError( + "Cannot catch D-Bus signal from class." + ) + yield + + __aiter__ = catch + + async def catch_anywhere( + self, + service_name: Optional[str] = None, + bus: Optional[SdBus] = None, + ) -> AsyncIterable[Tuple[str, T]]: + if service_name is None: + raise ValueError( + 'Called catch_anywhere from class ' + 'but service name was not provided.' + ) + + if bus is None: + bus = get_default_bus() + + message_queue = await bus.get_signal_queue_async( + service_name, + None, + self.dbus_signal.interface_name, + self.dbus_signal.signal_name, + ) + + while True: + next_signal_message = await message_queue.get() + signal_path = next_signal_message.path + assert signal_path is not None + yield ( + signal_path, + cast(T, next_signal_message.get_contents()) + ) + + def emit(self, args: T) -> None: + raise NotImplementedError( + "Cannot emit D-Bus signal from class." + ) + + def dbus_signal_async( signal_signature: str = '', signal_args_names: Sequence[str] = (), diff --git a/src/sdbus/utils.py b/src/sdbus/utils.py index fa106fd..cd7a42f 100644 --- a/src/sdbus/utils.py +++ b/src/sdbus/utils.py @@ -52,7 +52,7 @@ def parse_properties_changed( changed_properties_data[invalidated_property] = ('0', None) return _parse_properties_vardict( - interface._dbus_to_python_name_map, + interface._dbus_meta.dbus_member_to_python_attr, properties_changed_data[1], on_unknown_member, ) @@ -84,7 +84,7 @@ def _create_interfaces_map( isinstance(interface, DbusInterfaceBaseAsync) ): interfaces_to_class_map[ - frozenset(interface._dbus_served_interfaces_names) + frozenset(interface._dbus_meta.dbus_interfaces_names) ] = type(interface) elif ( isinstance(interface, type) @@ -92,7 +92,7 @@ def _create_interfaces_map( issubclass(interface, DbusInterfaceBaseAsync) ): interfaces_to_class_map[ - frozenset(interface._dbus_served_interfaces_names) + frozenset(interface._dbus_meta.dbus_interfaces_names) ] = interface else: raise TypeError('Expected D-Bus interface, got: ', interface) @@ -131,7 +131,9 @@ def parse_interfaces_added( class_set = frozenset(properties_data.keys()) - SKIP_INTERFACES try: python_class = interfaces_to_class_map[class_set] - dbus_to_python_member_map = python_class._dbus_to_python_name_map + dbus_to_python_member_map = ( + python_class._dbus_meta.dbus_member_to_python_attr + ) except KeyError: if on_unknown_interface == 'error': raise diff --git a/test/test_low_level_errors.py b/test/test_low_level_errors.py index b264e74..d8631ea 100644 --- a/test/test_low_level_errors.py +++ b/test/test_low_level_errors.py @@ -22,6 +22,7 @@ from asyncio import get_running_loop, wait_for from typing import Any +from sdbus.dbus_common_elements import DbusLocalObjectMeta from sdbus.exceptions import DbusFailedError from sdbus.unittest import IsolatedDbusTestCase @@ -164,7 +165,10 @@ async def test_property_setter_derived_error(self) -> None: await self.test_object_connection.hello_world() async def test_property_callback_error(self) -> None: - interface = self.test_object._activated_interfaces[0] + dbus_local_meta = self.test_object._dbus + if not isinstance(dbus_local_meta, DbusLocalObjectMeta): + raise TypeError + interface = dbus_local_meta.activated_interfaces[0] interface.property_get_dict.pop(b'DerriveErrSettable') with self.assertRaises(DbusFailedError): @@ -175,7 +179,10 @@ async def test_property_callback_error(self) -> None: async def test_method_callback_error(self) -> None: TEST_KEY = b'HelloWorld' - interface = self.test_object._activated_interfaces[0] + dbus_local_meta = self.test_object._dbus + if not isinstance(dbus_local_meta, DbusLocalObjectMeta): + raise TypeError + interface = dbus_local_meta.activated_interfaces[0] interface.method_dict.pop(TEST_KEY) with self.assertRaises(DbusFailedError): diff --git a/test/test_sdbus_async.py b/test/test_sdbus_async.py index 3ec3acc..74592d2 100644 --- a/test/test_sdbus_async.py +++ b/test/test_sdbus_async.py @@ -17,10 +17,9 @@ # 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 asyncio import Event, get_running_loop, sleep, wait_for +from asyncio import Event, get_running_loop, sleep, wait, wait_for from asyncio.subprocess import create_subprocess_exec from typing import TYPE_CHECKING, cast from unittest import SkipTest @@ -52,7 +51,8 @@ ) if TYPE_CHECKING: - from typing import Tuple + from asyncio import Task + from typing import Any, Tuple from sdbus.dbus_proxy_async_interfaces import ( DBUS_PROPERTIES_CHANGED_TYPING, @@ -350,18 +350,18 @@ def test_property_setter(self, var: str) -> None: with self.subTest('Test dbus to python mapping'): self.assertIn( - test_object.properties_changed.dbus_signal.signal_name, - test_object._dbus_to_python_name_map, + "PropertiesChanged", + test_object._dbus_meta.dbus_member_to_python_attr, ) self.assertIn( - test_subclass.properties_changed.dbus_signal.signal_name, - test_subclass._dbus_to_python_name_map, + "PropertiesChanged", + test_subclass._dbus_meta.dbus_member_to_python_attr, ) self.assertIn( - test_subclass.test_property.dbus_property.property_name, - test_subclass._dbus_to_python_name_map, + "TestProperty", + test_subclass._dbus_meta.dbus_member_to_python_attr, ) with self.subTest('Tripple subclass'): @@ -434,15 +434,17 @@ async def test_signal(self) -> None: test_tuple = ('sgfsretg', 'asd') - ai_dbus = test_object_connection.test_signal.__aiter__() - aw_dbus = ai_dbus.__anext__() - q = test_object.test_signal._get_local_queue() + aiter_dbus: Any = test_object_connection.test_signal.__aiter__() + anext_dbus: Task[Any] = loop.create_task(aiter_dbus.__anext__()) + aiter_local: Any = test_object.test_signal.__aiter__() + anext_local: Task[Any] = loop.create_task(aiter_local.__anext__()) - loop.call_at(0, test_object.test_signal.emit, test_tuple) + loop.call_later(0.1, test_object.test_signal.emit, test_tuple) - self.assertEqual(test_tuple, await wait_for(aw_dbus, timeout=1)) + await wait((anext_dbus, anext_local), timeout=1) - self.assertEqual(test_tuple, await wait_for(q.get(), timeout=1)) + self.assertEqual(test_tuple, anext_dbus.result()) + self.assertEqual(test_tuple, anext_local.result()) async def test_signal_catch_anywhere(self) -> None: test_object, test_object_connection = initialize_object() @@ -675,8 +677,7 @@ async def test_singal_queue_wildcard_match(self) -> None: test_object.test_signal.emit(('test', 'signal')) message = await wait_for(message_queue.get(), timeout=1) - self.assertEqual(message.member, - test_object.test_signal.dbus_signal.signal_name) + self.assertEqual(message.member, "TestSignal") async def test_class_with_string_subclass_parameter(self) -> None: from enum import Enum @@ -724,31 +725,40 @@ async def test_empty_signal(self) -> None: loop = get_running_loop() - ai_dbus = test_object_connection.empty_signal.__aiter__() - aw_dbus = ai_dbus.__anext__() - q = test_object.empty_signal._get_local_queue() + aiter_dbus: Any = test_object_connection.empty_signal.__aiter__() + anext_dbus: Task[Any] = loop.create_task(aiter_dbus.__anext__()) + aiter_local: Any = test_object.empty_signal.__aiter__() + anext_local: Task[Any] = loop.create_task(aiter_local.__anext__()) + + loop.call_later(0.1, test_object.empty_signal.emit, None) - loop.call_at(0, test_object.empty_signal.emit, None) + await wait((anext_dbus, anext_local), timeout=1) - self.assertIsNone(await wait_for(aw_dbus, timeout=1)) + self.assertIsNone(anext_dbus.result()) - self.assertIsNone(await wait_for(q.get(), timeout=1)) + self.assertIsNone(anext_local.result()) async def test_properties_changed(self) -> None: test_object, test_object_connection = initialize_object() test_str = 'should_be_emited' - q = await test_object_connection.properties_changed._get_dbus_queue() + properties_changed_dbus_aiter = ( + test_object_connection.properties_changed.__aiter__() + ) async def set_property() -> None: + await sleep(0.1) await test_object_connection.test_property.set_async(test_str) - await set_property() + get_running_loop().create_task(set_property()) properties_changed_data = cast( DBUS_PROPERTIES_CHANGED_TYPING, - (await q.get()).get_contents(), + await wait_for( + properties_changed_dbus_aiter.__anext__(), + timeout=1 + ), ) parsed_dict_from_class = parse_properties_changed( @@ -786,18 +796,27 @@ async def test_property_private_setter(self) -> None: await test_object_connection.test_property_private.set_async( new_value) - q = await test_object_connection.properties_changed._get_dbus_queue() + properties_changed_dbus_aiter = ( + test_object_connection.properties_changed.__aiter__() + ) - await test_object.test_property_private.set_async(new_value) + async def set_property() -> None: + await sleep(0.1) + await test_object.test_property_private.set_async(new_value) - self.assertEqual( - await test_object_connection.test_property_private, - new_value - ) + get_running_loop().create_task(set_property()) changed_properties = cast( DBUS_PROPERTIES_CHANGED_TYPING, - (await q.get()).get_contents(), + await wait_for( + properties_changed_dbus_aiter.__anext__(), + timeout=1, + ), + ) + + self.assertEqual( + await test_object_connection.test_property_private, + new_value ) self.assertIn('TestPropertyPrivate', changed_properties[1]) From bda45dfddd3597bebfbbf0fce67e3d51fed44b1e Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sat, 16 Dec 2023 20:29:25 +0600 Subject: [PATCH 055/188] setup.py: Add support for minor libsystemd versions like "252.9" --- setup.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 7b6a236..355962d 100644 --- a/setup.py +++ b/setup.py @@ -35,11 +35,14 @@ def get_libsystemd_version() -> int: stderr=DEVNULL, stdout=PIPE, check=True, + text=True, ) - result_str = process.stdout.decode('utf-8') + result_str = process.stdout + # Version can either be like 250 or 250.10 + first_component = result_str.split(".")[0] - return int(result_str) + return int(first_component) if not environ.get('PYTHON_SDBUS_USE_IGNORE_SYSTEMD_VERSION'): From 6bb0be6947892d910b8b1bec543b6bc3e740ca22 Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sat, 16 Dec 2023 20:34:24 +0600 Subject: [PATCH 056/188] ci: Do not use podman's --tty and --interactive options Those options are not useful when running CI and only raise warnings. --- .github/workflows/ubuntu_test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ubuntu_test.yml b/.github/workflows/ubuntu_test.yml index d9eb77a..28ab86b 100644 --- a/.github/workflows/ubuntu_test.yml +++ b/.github/workflows/ubuntu_test.yml @@ -70,7 +70,7 @@ jobs: podman build --tag alpine-ci -f ./test/containers/Containerfile-alpine . - name: Test unlimited API run: | - podman run --rm -it alpine-ci + podman run --rm alpine-ci - name: Test limited API run: | - podman run --env PYTHON_SDBUS_USE_LIMITED_API=1 --rm -it alpine-ci + podman run --env PYTHON_SDBUS_USE_LIMITED_API=1 --rm alpine-ci From e6c89c8d1e7be41dec792f640d19e63d4bcb12c1 Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sat, 16 Dec 2023 20:42:41 +0600 Subject: [PATCH 057/188] ci: Install Jinja template engine for Alpine testing images --- test/containers/Containerfile-alpine | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/containers/Containerfile-alpine b/test/containers/Containerfile-alpine index 73e3f34..984b152 100644 --- a/test/containers/Containerfile-alpine +++ b/test/containers/Containerfile-alpine @@ -13,7 +13,8 @@ RUN apk update && \ musl-dev \ gcc \ pkgconfig \ - dbus + dbus \ + py3-jinja2 WORKDIR /root/python-sdbus/ From 513fd920f496939dd72cb318f07003c6216995c5 Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sun, 17 Dec 2023 01:06:03 +0600 Subject: [PATCH 058/188] Add @setter_private support for property overrides This lets you define a private setter that can only be called from local object to the existing read only property. --- src/sdbus/dbus_common_elements.py | 8 +++ src/sdbus/dbus_proxy_async_interface_base.py | 3 + test/test_sdbus_async.py | 59 ++++++++++++++++++++ 3 files changed, 70 insertions(+) diff --git a/src/sdbus/dbus_common_elements.py b/src/sdbus/dbus_common_elements.py index 649b073..00e7b5c 100644 --- a/src/sdbus/dbus_common_elements.py +++ b/src/sdbus/dbus_common_elements.py @@ -299,10 +299,18 @@ class DbusOverload: def __init__(self, original: T): self.original = original self.setter_overload: Optional[Callable[[Any, T], None]] = None + self.is_setter_public = True def setter(self, new_setter: Optional[Callable[[Any, T], None]]) -> None: self.setter_overload = new_setter + def setter_private( + self, + new_setter: Optional[Callable[[Any, T], None]], + ) -> None: + self.setter_overload = new_setter + self.is_setter_public = False + class DbusRemoteObjectMeta: def __init__( diff --git a/src/sdbus/dbus_proxy_async_interface_base.py b/src/sdbus/dbus_proxy_async_interface_base.py index 542eab5..a455192 100644 --- a/src/sdbus/dbus_proxy_async_interface_base.py +++ b/src/sdbus/dbus_proxy_async_interface_base.py @@ -172,6 +172,9 @@ def __new__(cls, name: str, dbus_element_override.property_setter = ( override.setter_overload ) + dbus_element_override.property_setter_is_public = ( + override.is_setter_public + ) else: raise TypeError( f"Unknown override {collision_name!r} " diff --git a/test/test_sdbus_async.py b/test/test_sdbus_async.py index 74592d2..1889043 100644 --- a/test/test_sdbus_async.py +++ b/test/test_sdbus_async.py @@ -820,3 +820,62 @@ async def set_property() -> None: ) self.assertIn('TestPropertyPrivate', changed_properties[1]) + + async def test_property_override_setter_private(self) -> None: + + test_int = 1 + + class TestInterfacePrivateSetter(TestInterface): + @dbus_property_async_override() + def test_property_private(self) -> int: + return test_int + + @test_property_private.setter_private + def _private_setter(self, new_value: int) -> None: + nonlocal test_int + test_int = new_value + + test_object = TestInterfacePrivateSetter() + test_object.export_to_dbus('/') + test_object_connection = TestInterface.new_proxy( + TEST_SERVICE_NAME, '/') + + self.assertEqual( + await test_object_connection.test_property_private, + test_int, + ) + + async def catch_properties_changed() -> int: + async for x in test_object_connection.properties_changed: + changed_attr = parse_properties_changed( + TestInterface, x)["test_property_private"] + + if not isinstance(changed_attr, int): + raise TypeError + + return changed_attr + + raise RuntimeError + + catch_changed_task = get_running_loop( + ).create_task(catch_properties_changed()) + + with self.assertRaises(DbusPropertyReadOnlyError): + await test_object_connection.test_property_private.set_async(10) + + await test_object.test_property_private.set_async(10) + + self.assertEqual( + await test_object_connection.test_property_private, + 10, + ) + + self.assertEqual( + await test_object.test_property_private, + test_int, + ) + + self.assertEqual( + await wait_for(catch_changed_task, timeout=1), + 10, + ) From 1708748db4b4eb43f6c50fd9acdf143aa4ec85e3 Mon Sep 17 00:00:00 2001 From: igo95862 Date: Fri, 29 Dec 2023 21:45:51 +0600 Subject: [PATCH 059/188] Fix unable to set the method args names for methods without return args The default value for D-Bus method argument names is now None which enables setting the return argument names to an empty sequence. For example, empty tuple `()`. --- src/sdbus/dbus_common_elements.py | 34 +++--- src/sdbus/dbus_proxy_async_method.py | 4 +- test/test_sdbus_async_introspection.py | 144 +++++++++++++++++++++++++ 3 files changed, 165 insertions(+), 17 deletions(-) create mode 100644 test/test_sdbus_async_introspection.py diff --git a/src/sdbus/dbus_common_elements.py b/src/sdbus/dbus_common_elements.py index 00e7b5c..cbc6564 100644 --- a/src/sdbus/dbus_common_elements.py +++ b/src/sdbus/dbus_common_elements.py @@ -119,9 +119,9 @@ def __init__( original_method: FunctionType, method_name: Optional[str], input_signature: str, - input_args_names: Sequence[str], + input_args_names: Optional[Sequence[str]], result_signature: str, - result_args_names: Sequence[str], + result_args_names: Optional[Sequence[str]], flags: int): assert not isinstance(input_args_names, str), ( @@ -129,14 +129,6 @@ def __init__( " names. Did you forget to put" " it in to a tuple ('string', ) ?") - assert not any(' ' in x for x in input_args_names), ( - "Can't have spaces in argument input names" - f"Args: {input_args_names}") - - assert not any(' ' in x for x in result_args_names), ( - "Can't have spaces in argument result names." - f"Args: {result_args_names}") - if method_name is None: method_name = ''.join( _method_name_converter(original_method.__name__)) @@ -162,13 +154,25 @@ def __init__( self.method_name = method_name self.input_signature = input_signature - self.input_args_names: Sequence[str] = ( - self.args_names - if result_args_names and not input_args_names - else input_args_names) + self.input_args_names: Sequence[str] = () + if input_args_names is not None: + assert not any(' ' in x for x in input_args_names), ( + "Can't have spaces in argument input names" + f"Args: {input_args_names}") + + self.input_args_names = input_args_names + elif result_args_names is not None: + self.input_args_names = self.args_names self.result_signature = result_signature - self.result_args_names = result_args_names + self.result_args_names: Sequence[str] = () + if result_args_names is not None: + assert not any(' ' in x for x in result_args_names), ( + "Can't have spaces in argument result names." + f"Args: {result_args_names}") + + self.result_args_names = result_args_names + self.flags = flags self.__doc__ = original_method.__doc__ diff --git a/src/sdbus/dbus_proxy_async_method.py b/src/sdbus/dbus_proxy_async_method.py index 3d7a900..ffd41ee 100644 --- a/src/sdbus/dbus_proxy_async_method.py +++ b/src/sdbus/dbus_proxy_async_method.py @@ -231,8 +231,8 @@ def dbus_method_async( input_signature: str = "", result_signature: str = "", flags: int = 0, - result_args_names: Sequence[str] = (), - input_args_names: Sequence[str] = (), + result_args_names: Optional[Sequence[str]] = None, + input_args_names: Optional[Sequence[str]] = None, method_name: Optional[str] = None, ) -> Callable[[T], T]: diff --git a/test/test_sdbus_async_introspection.py b/test/test_sdbus_async_introspection.py new file mode 100644 index 0000000..20625be --- /dev/null +++ b/test/test_sdbus_async_introspection.py @@ -0,0 +1,144 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later + +# Copyright (C) 2023 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 typing import TYPE_CHECKING + +from sdbus.unittest import IsolatedDbusTestCase + +from sdbus import DbusInterfaceCommonAsync, dbus_method_async + +if TYPE_CHECKING: + from typing import Tuple, Type + +TEST_SERVICE_NAME = 'org.example.test' + + +def initialize_object( + interface_class: Type[DbusInterfaceCommonAsync], +) -> Tuple[DbusInterfaceCommonAsync, DbusInterfaceCommonAsync]: + test_object = interface_class() + test_object.export_to_dbus('/') + + test_object_connection = interface_class.new_proxy( + TEST_SERVICE_NAME, '/') + + return test_object, test_object_connection + + +class TestIntrospection(IsolatedDbusTestCase): + + async def asyncSetUp(self) -> None: + await super().asyncSetUp() + await self.bus.request_name_async("org.example.test", 0) + + async def test_method_arg_names_none(self) -> None: + class TestInterface( + DbusInterfaceCommonAsync, + interface_name="org.test.test", + ): + @dbus_method_async( + input_signature="ss", + result_signature="i", + ) + async def login( + self, + user_name: str, + pin_code: str, + ) -> int: + return 0 + + obj, rem = initialize_object(TestInterface) + + introspection = await rem.dbus_introspect() + self.assertNotIn('name="user_name"', introspection) + self.assertNotIn('name="result"', introspection) + self.assertNotIn('name="pin_code"', introspection) + + async def test_method_arg_names_result_names_only(self) -> None: + class TestInterface( + DbusInterfaceCommonAsync, + interface_name="org.test.test", + ): + @dbus_method_async( + input_signature="ss", + result_signature="i", + result_args_names=("result",) + ) + async def login( + self, + user_name: str, + pin_code: str, + ) -> int: + return 0 + + obj, rem = initialize_object(TestInterface) + + introspection = await rem.dbus_introspect() + self.assertIn('name="user_name"', introspection) + self.assertIn('name="result"', introspection) + self.assertIn('name="pin_code"', introspection) + + async def test_method_arg_names_full(self) -> None: + class TestInterface( + DbusInterfaceCommonAsync, + interface_name="org.test.test", + ): + @dbus_method_async( + input_signature="ss", + input_args_names=("UserName", "PinCode"), + result_signature="i", + result_args_names=("Result",) + ) + async def login( + self, + user_name: str, + pin_code: str, + ) -> int: + return 0 + + obj, rem = initialize_object(TestInterface) + + introspection = await rem.dbus_introspect() + self.assertIn('name="UserName"', introspection) + self.assertIn('name="Result"', introspection) + self.assertIn('name="PinCode"', introspection) + + async def test_method_arg_names_no_return_args(self) -> None: + class TestInterface( + DbusInterfaceCommonAsync, + interface_name="org.test.test", + ): + @dbus_method_async( + input_signature="ss", + result_args_names=(), + ) + async def login( + self, + user_name: str, + pin_code: str, + ) -> None: + return None + + obj, rem = initialize_object(TestInterface) + + introspection = await rem.dbus_introspect() + self.assertIn('name="user_name"', introspection) + self.assertIn('name="pin_code"', introspection) From 63d77b4dcd11b743f6e1794be8a7d6f78845f126 Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sat, 30 Dec 2023 15:52:43 +0600 Subject: [PATCH 060/188] docs: Fix `catch_anywhere` example missing the `catch_anywhere` call Simply calling from the class does not make it `catch_anywhere`. --- docs/asyncio_quick.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/asyncio_quick.rst b/docs/asyncio_quick.rst index 37ec726..b39331a 100644 --- a/docs/asyncio_quick.rst +++ b/docs/asyncio_quick.rst @@ -280,7 +280,7 @@ the service name must be provided. Example:: - async for path, x in ExampleInterface.name_changed('org.example.test'): + async for path, x in ExampleInterface.name_changed.catch_anywhere('org.example.test'): print(f"On {path} caught: {x}") Subclass Overrides From 8b11d3f7870c4cece9d0c47430e2fff01e55ae3a Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sun, 31 Dec 2023 22:49:26 +0600 Subject: [PATCH 061/188] Rework signals integration with libsystemd Instead of relying on asyncio Queues use the `call_soon` of event loop to schedule callbacks. The callback should be a blocking function that accepts an SdBusMessage and returns None. The `get_signal_queue_async` of SdBus was renamed to `match_signal_async` which matches the sd-bus call used. Callback system is more flexible. The existing signal API will create the asyncio.Queue inside the `catch` methods and register the callbacks to the `put_nowait` method of the Queue. This means the code using public API is fully backwards compatible. --- src/sdbus/dbus_proxy_async_signal.py | 59 ++++++++++++++---------- src/sdbus/sd_bus_internals.c | 18 ++++++-- src/sdbus/sd_bus_internals.h | 2 - src/sdbus/sd_bus_internals.py | 12 +++-- src/sdbus/sd_bus_internals_bus.c | 69 ++++++++++++---------------- test/test_sdbus_async.py | 18 ++++++-- 6 files changed, 99 insertions(+), 79 deletions(-) diff --git a/src/sdbus/dbus_proxy_async_signal.py b/src/sdbus/dbus_proxy_async_signal.py index 369d9a1..426db7d 100644 --- a/src/sdbus/dbus_proxy_async_signal.py +++ b/src/sdbus/dbus_proxy_async_signal.py @@ -20,6 +20,7 @@ from __future__ import annotations from asyncio import Queue +from contextlib import closing from types import FunctionType from typing import ( TYPE_CHECKING, @@ -43,7 +44,7 @@ from typing import Any, Callable, Optional, Sequence, Tuple, Type from .dbus_proxy_async_interface_base import DbusInterfaceBaseAsync - from .sd_bus_internals import SdBus + from .sd_bus_internals import SdBus, SdBusMessage T = TypeVar('T') @@ -97,16 +98,20 @@ def __init__( self.__doc__ = dbus_signal.__doc__ async def catch(self) -> AsyncIterator[T]: - dbus_queue = await self.proxy_meta.attached_bus.get_signal_queue_async( + message_queue: Queue[SdBusMessage] = Queue() + + match_slot = await self.proxy_meta.attached_bus.match_signal_async( self.proxy_meta.service_name, self.proxy_meta.object_path, self.dbus_signal.interface_name, self.dbus_signal.signal_name, + message_queue.put_nowait, ) - while True: - next_signal_message = await dbus_queue.get() - yield cast(T, next_signal_message.get_contents()) + with closing(match_slot): + while True: + next_signal_message = await message_queue.get() + yield cast(T, next_signal_message.get_contents()) __aiter__ = catch @@ -121,21 +126,25 @@ async def catch_anywhere( if service_name is None: service_name = self.proxy_meta.service_name - message_queue = await bus.get_signal_queue_async( + message_queue: Queue[SdBusMessage] = Queue() + + match_slot = await bus.match_signal_async( service_name, None, self.dbus_signal.interface_name, self.dbus_signal.signal_name, + message_queue.put_nowait, ) - while True: - next_signal_message = await message_queue.get() - signal_path = next_signal_message.path - assert signal_path is not None - yield ( - signal_path, - cast(T, next_signal_message.get_contents()) - ) + with closing(match_slot): + while True: + next_signal_message = await message_queue.get() + signal_path = next_signal_message.path + assert signal_path is not None + yield ( + signal_path, + cast(T, next_signal_message.get_contents()) + ) def emit(self, args: T) -> None: raise RuntimeError("Cannot emit signal from D-Bus proxy.") @@ -264,21 +273,25 @@ async def catch_anywhere( if bus is None: bus = get_default_bus() - message_queue = await bus.get_signal_queue_async( + message_queue: Queue[SdBusMessage] = Queue() + + match_slot = await bus.match_signal_async( service_name, None, self.dbus_signal.interface_name, self.dbus_signal.signal_name, + message_queue.put_nowait, ) - while True: - next_signal_message = await message_queue.get() - signal_path = next_signal_message.path - assert signal_path is not None - yield ( - signal_path, - cast(T, next_signal_message.get_contents()) - ) + with closing(match_slot): + while True: + next_signal_message = await message_queue.get() + signal_path = next_signal_message.path + assert signal_path is not None + yield ( + signal_path, + cast(T, next_signal_message.get_contents()) + ) def emit(self, args: T) -> None: raise NotImplementedError( diff --git a/src/sdbus/sd_bus_internals.c b/src/sdbus/sd_bus_internals.c index 0eb4a02..55965a5 100644 --- a/src/sdbus/sd_bus_internals.c +++ b/src/sdbus/sd_bus_internals.c @@ -22,12 +22,10 @@ // Python functions and objects PyObject* asyncio_get_running_loop = NULL; -PyObject* asyncio_queue_class = NULL; PyObject* is_coroutine_function = NULL; // Str objects PyObject* set_result_str = NULL; PyObject* set_exception_str = NULL; -PyObject* put_no_wait_str = NULL; PyObject* add_reader_str = NULL; PyObject* remove_reader_str = NULL; PyObject* empty_str = NULL; @@ -56,6 +54,18 @@ static void SdBusSlot_dealloc(SdBusSlotObject* self) { SD_BUS_DEALLOC_TAIL; } +static PyObject* SdBusSlot_close(SdBusSlotObject* self) { + sd_bus_slot_unref(self->slot_ref); + self->slot_ref = NULL; + + Py_RETURN_NONE; +} + +static PyMethodDef SdBusSlot_methods[] = { + {"close", (PyCFunction)SdBusSlot_close, METH_NOARGS, PyDoc_STR("Dereference sd-bus slot stopping any associated callbacks.")}, + {NULL, NULL, 0, NULL}, +}; + PyType_Spec SdBusSlotType = { .name = "sd_bus_internals.SdBusSlot", .basicsize = sizeof(SdBusSlotObject), @@ -65,6 +75,7 @@ PyType_Spec SdBusSlotType = { (PyType_Slot[]){ {Py_tp_new, PyType_GenericNew}, {Py_tp_dealloc, (destructor)SdBusSlot_dealloc}, + {Py_tp_methods, SdBusSlot_methods}, {0, NULL}, }, }; @@ -154,11 +165,8 @@ PyMODINIT_FUNC PyInit_sd_bus_internals(void) { asyncio_get_running_loop = CALL_PYTHON_AND_CHECK(PyObject_GetAttrString(asyncio_module, "get_running_loop")); - asyncio_queue_class = CALL_PYTHON_AND_CHECK(PyObject_GetAttrString(asyncio_module, "Queue")); - set_result_str = CALL_PYTHON_AND_CHECK(PyUnicode_FromString("set_result")); set_exception_str = CALL_PYTHON_AND_CHECK(PyUnicode_FromString("set_exception")); - put_no_wait_str = CALL_PYTHON_AND_CHECK(PyUnicode_FromString("put_nowait")); call_soon_str = CALL_PYTHON_AND_CHECK(PyUnicode_FromString("call_soon")); create_task_str = CALL_PYTHON_AND_CHECK(PyUnicode_FromString("create_task")); remove_reader_str = CALL_PYTHON_AND_CHECK(PyUnicode_FromString("remove_reader")); diff --git a/src/sdbus/sd_bus_internals.h b/src/sdbus/sd_bus_internals.h index 7fbe24b..0e90399 100644 --- a/src/sdbus/sd_bus_internals.h +++ b/src/sdbus/sd_bus_internals.h @@ -240,12 +240,10 @@ // Python functions and objects extern PyObject* asyncio_get_running_loop; -extern PyObject* asyncio_queue_class; extern PyObject* is_coroutine_function; // Str objects extern PyObject* set_result_str; extern PyObject* set_exception_str; -extern PyObject* put_no_wait_str; extern PyObject* add_reader_str; extern PyObject* remove_reader_str; extern PyObject* empty_str; diff --git a/src/sdbus/sd_bus_internals.py b/src/sdbus/sd_bus_internals.py index 7e2d806..9bf21cb 100644 --- a/src/sdbus/sd_bus_internals.py +++ b/src/sdbus/sd_bus_internals.py @@ -19,7 +19,7 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA from __future__ import annotations -from asyncio import Future, Queue +from asyncio import Future from typing import TYPE_CHECKING if TYPE_CHECKING: @@ -53,7 +53,9 @@ class SdBusSlot: """Holds reference to SdBus slot""" - ... + + def close(self) -> None: + raise NotImplementedError(__STUB_ERROR) class SdBusInterface: @@ -190,12 +192,12 @@ def add_interface(self, new_interface: SdBusInterface, object_path: str, interface_name: str, /) -> None: raise NotImplementedError(__STUB_ERROR) - def get_signal_queue_async( + def match_signal_async( self, senders_name: Optional[str], object_path: Optional[str], interface_name: Optional[str], member_name: Optional[str], - / - ) -> Future[Queue[SdBusMessage]]: + callback: Callable[[SdBusMessage], None], / + ) -> Future[SdBusSlot]: raise NotImplementedError(__STUB_ERROR) def request_name_async(self, name: str, flags: int, /) -> Future[None]: diff --git a/src/sdbus/sd_bus_internals_bus.c b/src/sdbus/sd_bus_internals_bus.c index aa6aec9..819159f 100644 --- a/src/sdbus/sd_bus_internals_bus.c +++ b/src/sdbus/sd_bus_internals_bus.c @@ -376,17 +376,16 @@ static PyObject* SdBus_add_interface(SdBusObject* self, PyObject* args) { } int _SdBus_signal_callback(sd_bus_message* m, void* userdata, sd_bus_error* Py_UNUSED(ret_error)) { - PyObject* async_queue = userdata; + PyObject* signal_callback = userdata; - SdBusMessageObject* new_message_object CLEANUP_SD_BUS_MESSAGE = (SdBusMessageObject*)SD_BUS_PY_CLASS_DUNDER_NEW(SdBusMessage_class); - if (new_message_object == NULL) { - return -1; - } + PyObject* running_loop CLEANUP_PY_OBJECT = CALL_PYTHON_CHECK_RETURN_NEG1(PyObject_CallFunctionObjArgs(asyncio_get_running_loop, NULL)); + + SdBusMessageObject* new_message_object CLEANUP_SD_BUS_MESSAGE = + (SdBusMessageObject*)CALL_PYTHON_CHECK_RETURN_NEG1(SD_BUS_PY_CLASS_DUNDER_NEW(SdBusMessage_class)); _SdBusMessage_set_messsage(new_message_object, m); - PyObject* should_be_none CLEANUP_PY_OBJECT = PyObject_CallMethodObjArgs(async_queue, put_no_wait_str, new_message_object, NULL); - if (should_be_none == NULL) { - return -1; - } + + Py_XDECREF(CALL_PYTHON_CHECK_RETURN_NEG1(PyObject_CallMethodObjArgs(running_loop, call_soon_str, signal_callback, new_message_object, NULL))); + return 0; } @@ -394,21 +393,15 @@ int _SdBus_match_signal_instant_callback(sd_bus_message* m, void* userdata, sd_b PyObject* new_future = userdata; if (!sd_bus_message_is_method_error(m, NULL)) { - PyObject* new_queue CLEANUP_PY_OBJECT = PyObject_GetAttrString(new_future, "_sd_bus_queue"); - if (new_queue == NULL) { - return -1; - } + SdBusSlotObject* slot_object CLEANUP_SD_BUS_SLOT = + (SdBusSlotObject*)CALL_PYTHON_CHECK_RETURN_NEG1(PyObject_GetAttrString(new_future, "_sd_bus_slot")); - PyObject* should_be_none CLEANUP_PY_OBJECT = PyObject_CallMethodObjArgs(new_future, set_result_str, new_queue, NULL); - if (should_be_none == NULL) { - return -1; - } + Py_XDECREF(CALL_PYTHON_CHECK_RETURN_NEG1(PyObject_CallMethodObjArgs(new_future, set_result_str, slot_object, NULL))); - SdBusSlotObject* slot_object CLEANUP_SD_BUS_SLOT = (SdBusSlotObject*)PyObject_GetAttrString(new_queue, "_sd_bus_slot"); - if (slot_object == NULL) { - return -1; - } - sd_bus_slot_set_userdata(slot_object->slot_ref, new_queue); + PyObject* signal_callback = CALL_PYTHON_CHECK_RETURN_NEG1(PyObject_GetAttrString(new_future, "_sd_bus_signal_callback")); + + sd_bus_slot_set_userdata(slot_object->slot_ref, signal_callback); + sd_bus_slot_set_destroy_callback(slot_object->slot_ref, (sd_bus_destroy_t)Py_DecRef); } else { if (future_set_exception_from_message(new_future, m) < 0) { return -1; @@ -424,40 +417,38 @@ static int _unicode_or_none(PyObject* some_object) { return (PyUnicode_Check(some_object) || (Py_None == some_object)); } -static PyObject* SdBus_get_signal_queue(SdBusObject* self, PyObject* const* args, Py_ssize_t nargs) { - SD_BUS_PY_CHECK_ARGS_NUMBER(4); +static PyObject* SdBus_match_signal_async(SdBusObject* self, PyObject* const* args, Py_ssize_t nargs) { + SD_BUS_PY_CHECK_ARGS_NUMBER(5); SD_BUS_PY_CHECK_ARG_CHECK_FUNC(0, _unicode_or_none); SD_BUS_PY_CHECK_ARG_CHECK_FUNC(1, _unicode_or_none); SD_BUS_PY_CHECK_ARG_CHECK_FUNC(2, _unicode_or_none); SD_BUS_PY_CHECK_ARG_CHECK_FUNC(3, _unicode_or_none); + SD_BUS_PY_CHECK_ARG_CHECK_FUNC(4, PyCallable_Check); const char* sender_service_char_ptr = SD_BUS_PY_UNICODE_AS_CHAR_PTR_OPTIONAL(args[0]); const char* path_name_char_ptr = SD_BUS_PY_UNICODE_AS_CHAR_PTR_OPTIONAL(args[1]); const char* interface_name_char_ptr = SD_BUS_PY_UNICODE_AS_CHAR_PTR_OPTIONAL(args[2]); const char* member_name_char_ptr = SD_BUS_PY_UNICODE_AS_CHAR_PTR_OPTIONAL(args[3]); + PyObject* signal_callback = args[4]; #else -static PyObject* SdBus_get_signal_queue(SdBusObject* self, PyObject* args) { +static PyObject* SdBus_match_signal_async(SdBusObject* self, PyObject* args) { const char* sender_service_char_ptr = NULL; const char* path_name_char_ptr = NULL; const char* interface_name_char_ptr = NULL; const char* member_name_char_ptr = NULL; - CALL_PYTHON_BOOL_CHECK( - PyArg_ParseTuple(args, "zzzz", &sender_service_char_ptr, &path_name_char_ptr, &interface_name_char_ptr, &member_name_char_ptr, NULL)); + PyObject* signal_callback = NULL; + CALL_PYTHON_BOOL_CHECK(PyArg_ParseTuple(args, "zzzzO", &sender_service_char_ptr, &path_name_char_ptr, &interface_name_char_ptr, &member_name_char_ptr, + &signal_callback, NULL)); #endif - SdBusSlotObject* new_slot CLEANUP_SD_BUS_SLOT = (SdBusSlotObject*)CALL_PYTHON_AND_CHECK(SD_BUS_PY_CLASS_DUNDER_NEW(SdBusSlot_class)); - - PyObject* new_queue CLEANUP_PY_OBJECT = CALL_PYTHON_AND_CHECK(PyObject_CallFunctionObjArgs(asyncio_queue_class, NULL)); - - // Bind lifetime of the slot to the queue - CALL_PYTHON_INT_CHECK(PyObject_SetAttrString(new_queue, "_sd_bus_slot", (PyObject*)new_slot)); - PyObject* running_loop CLEANUP_PY_OBJECT = CALL_PYTHON_AND_CHECK(PyObject_CallFunctionObjArgs(asyncio_get_running_loop, NULL)); - PyObject* new_future CLEANUP_PY_OBJECT = CALL_PYTHON_AND_CHECK(PyObject_CallMethod(running_loop, "create_future", "")); - // Bind lifetime of the queue to future - CALL_PYTHON_INT_CHECK(PyObject_SetAttrString(new_future, "_sd_bus_queue", new_queue)); + SdBusSlotObject* new_slot CLEANUP_SD_BUS_SLOT = (SdBusSlotObject*)CALL_PYTHON_AND_CHECK(SD_BUS_PY_CLASS_DUNDER_NEW(SdBusSlot_class)); + + // Bind lifetime of the slot to the Future + CALL_PYTHON_INT_CHECK(PyObject_SetAttrString(new_future, "_sd_bus_slot", (PyObject*)new_slot)); + CALL_PYTHON_INT_CHECK(PyObject_SetAttrString(new_future, "_sd_bus_signal_callback", signal_callback)); CALL_SD_BUS_AND_CHECK(sd_bus_match_signal_async(self->sd_bus_ref, &new_slot->slot_ref, sender_service_char_ptr, path_name_char_ptr, interface_name_char_ptr, member_name_char_ptr, _SdBus_signal_callback, @@ -654,8 +645,8 @@ static PyMethodDef SdBus_methods[] = { {"new_property_set_message", (SD_BUS_PY_FUNC_TYPE)SdBus_new_property_set_message, SD_BUS_PY_METH, PyDoc_STR("Create new empty property set message.")}, {"new_signal_message", (SD_BUS_PY_FUNC_TYPE)SdBus_new_signal_message, SD_BUS_PY_METH, PyDoc_STR("Create new empty signal message.")}, {"add_interface", (SD_BUS_PY_FUNC_TYPE)SdBus_add_interface, SD_BUS_PY_METH, PyDoc_STR("Add interface to the bus.")}, - {"get_signal_queue_async", (SD_BUS_PY_FUNC_TYPE)SdBus_get_signal_queue, SD_BUS_PY_METH, - PyDoc_STR("Returns a future that returns a queue that queues signal messages.")}, + {"match_signal_async", (SD_BUS_PY_FUNC_TYPE)SdBus_match_signal_async, SD_BUS_PY_METH, + PyDoc_STR("Register signal callback asynchronously. Returns a Future that returns a SdBusSlot.")}, {"request_name_async", (SD_BUS_PY_FUNC_TYPE)SdBus_request_name_async, SD_BUS_PY_METH, PyDoc_STR("Request D-Bus name async.")}, {"request_name", (SD_BUS_PY_FUNC_TYPE)SdBus_request_name, SD_BUS_PY_METH, PyDoc_STR("Request D-Bus name blocking.")}, {"add_object_manager", (SD_BUS_PY_FUNC_TYPE)SdBus_add_object_manager, SD_BUS_PY_METH, PyDoc_STR("Add object manager at the path.")}, diff --git a/test/test_sdbus_async.py b/test/test_sdbus_async.py index 1889043..6a25340 100644 --- a/test/test_sdbus_async.py +++ b/test/test_sdbus_async.py @@ -670,14 +670,22 @@ async def too_long_wait() -> None: async def test_singal_queue_wildcard_match(self) -> None: test_object, test_object_connection = initialize_object() - message_queue = await self.bus.get_signal_queue_async( + loop = get_running_loop() + future = loop.create_future() + + slot = await self.bus.match_signal_async( TEST_SERVICE_NAME, - None, None, None) + None, None, None, + future.set_result) - test_object.test_signal.emit(('test', 'signal')) + try: + test_object.test_signal.emit(('test', 'signal')) - message = await wait_for(message_queue.get(), timeout=1) - self.assertEqual(message.member, "TestSignal") + await wait_for(future, timeout=1) + message = future.result() + self.assertEqual(message.member, "TestSignal") + finally: + slot.close() async def test_class_with_string_subclass_parameter(self) -> None: from enum import Enum From e7bd8dc1bc47ca50089efc8efc8a31a67cccf645 Mon Sep 17 00:00:00 2001 From: igo95862 Date: Thu, 4 Jan 2024 23:21:32 +0600 Subject: [PATCH 062/188] Fix interface compositioning not working because of common interfaces The issue is that any interface has the interfaces like `org.freedesktop.DBus.Introspectable` or `org.freedesktop.DBus.Properties`. The interface names and member names of those built-in interfaces should not be checked for collisions. Thank you @AndersBlomdell for testing NetworkManager binds against development version. --- src/sdbus/dbus_proxy_async_interface_base.py | 3 +++ src/sdbus/dbus_proxy_sync_interface_base.py | 5 +++- test/test_sdbus_async.py | 24 ++++++++++++++++++-- test/test_sdbus_block.py | 22 ++++++++++++++++++ 4 files changed, 51 insertions(+), 3 deletions(-) diff --git a/src/sdbus/dbus_proxy_async_interface_base.py b/src/sdbus/dbus_proxy_async_interface_base.py index a455192..1b686b3 100644 --- a/src/sdbus/dbus_proxy_async_interface_base.py +++ b/src/sdbus/dbus_proxy_async_interface_base.py @@ -97,6 +97,9 @@ def __new__(cls, name: str, f"async interface: {attr_name!r}" ) + if not serving_enabled: + continue + if isinstance(attr, DbusMethodAsync): dbus_class_meta.dbus_member_to_python_attr[ attr.method_name] = attr_name diff --git a/src/sdbus/dbus_proxy_sync_interface_base.py b/src/sdbus/dbus_proxy_sync_interface_base.py index 02e5846..b38688f 100644 --- a/src/sdbus/dbus_proxy_sync_interface_base.py +++ b/src/sdbus/dbus_proxy_sync_interface_base.py @@ -46,7 +46,7 @@ def __new__(cls, name: str, ) -> DbusInterfaceMetaSync: dbus_class_meta = DbusClassMeta() - if interface_name is not None: + if interface_name is not None and serving_enabled: dbus_class_meta.dbus_interfaces_names.add(interface_name) for attr_name, attr in namespace.items(): @@ -58,6 +58,9 @@ def __new__(cls, name: str, f"Can't mix async methods in sync interface: {attr_name!r}" ) + if not serving_enabled: + continue + if isinstance(attr, DbusMethodSync): dbus_class_meta.dbus_member_to_python_attr[ attr.method_name] = attr_name diff --git a/test/test_sdbus_async.py b/test/test_sdbus_async.py index 6a25340..9a04228 100644 --- a/test/test_sdbus_async.py +++ b/test/test_sdbus_async.py @@ -350,12 +350,12 @@ def test_property_setter(self, var: str) -> None: with self.subTest('Test dbus to python mapping'): self.assertIn( - "PropertiesChanged", + "TestInt", test_object._dbus_meta.dbus_member_to_python_attr, ) self.assertIn( - "PropertiesChanged", + "TestInt", test_subclass._dbus_meta.dbus_member_to_python_attr, ) @@ -887,3 +887,23 @@ async def catch_properties_changed() -> int: await wait_for(catch_changed_task, timeout=1), 10, ) + + async def test_interface_composition(self) -> None: + class OneInterface( + DbusInterfaceCommonAsync, + interface_name="org.example.one", + ): + @dbus_method_async(result_signature="x") + async def one(self) -> int: + raise NotImplementedError + + class TwoInterface( + DbusInterfaceCommonAsync, + interface_name="org.example.two", + ): + @dbus_method_async(result_signature="t") + async def two(self) -> int: + return 2 + + class CombinedInterface(OneInterface, TwoInterface): + ... diff --git a/test/test_sdbus_block.py b/test/test_sdbus_block.py index a0d7f0f..a3f9f66 100644 --- a/test/test_sdbus_block.py +++ b/test/test_sdbus_block.py @@ -26,6 +26,8 @@ from sdbus.unittest import IsolatedDbusTestCase from sdbus_block.dbus_daemon import FreedesktopDbus +from sdbus import DbusInterfaceCommon, dbus_method + class TestSync(IsolatedDbusTestCase): @@ -70,6 +72,26 @@ def test_docstring(self) -> None: with self.subTest('Property doc (through class dict)'): self.assertTrue(getdoc(s.__class__.__dict__['features'])) + def test_interface_composition(self) -> None: + class OneInterface( + DbusInterfaceCommon, + interface_name="org.example.one", + ): + @dbus_method(result_signature="x") + def one(self) -> int: + raise NotImplementedError + + class TwoInterface( + DbusInterfaceCommon, + interface_name="org.example.two", + ): + @dbus_method(result_signature="t") + def two(self) -> int: + raise NotImplementedError + + class CombinedInterface(OneInterface, TwoInterface): + ... + if __name__ == '__main__': main() From 25661bd4837545d1edc841d6fa818f31d7bd3b09 Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sat, 13 Jan 2024 22:32:17 +0600 Subject: [PATCH 063/188] Use callbacks for local objects signals This is a more flexible approach and also avoids allocating a dict for every initialized object. Instead a weakref set will be allocated for every signal. (maybe optimized later with lazy allocation) --- src/sdbus/dbus_common_elements.py | 3 -- src/sdbus/dbus_proxy_async_signal.py | 54 +++++++++++++--------------- 2 files changed, 25 insertions(+), 32 deletions(-) diff --git a/src/sdbus/dbus_common_elements.py b/src/sdbus/dbus_common_elements.py index cbc6564..1ec287d 100644 --- a/src/sdbus/dbus_common_elements.py +++ b/src/sdbus/dbus_common_elements.py @@ -30,7 +30,6 @@ from .sd_bus_internals import is_interface_name_valid, is_member_name_valid if TYPE_CHECKING: - from asyncio import Queue from types import FunctionType from typing import ( Any, @@ -336,8 +335,6 @@ def __init__(self) -> None: self.activated_interfaces: List[SdBusInterface] = [] self.serving_object_path: Optional[str] = None self.attached_bus: Optional[SdBus] = None - self.local_signal_queues: Dict[ - Tuple[str, str], Set[Queue[Any]]] = {} class DbusClassMeta: diff --git a/src/sdbus/dbus_proxy_async_signal.py b/src/sdbus/dbus_proxy_async_signal.py index 426db7d..8c55583 100644 --- a/src/sdbus/dbus_proxy_async_signal.py +++ b/src/sdbus/dbus_proxy_async_signal.py @@ -30,6 +30,7 @@ TypeVar, cast, ) +from weakref import WeakSet from .dbus_common_elements import ( DbusBindedAsync, @@ -52,6 +53,24 @@ class DbusSignalAsync(DbusSomethingAsync, DbusSingalCommon, Generic[T]): + def __init__( + self, + signal_name: Optional[str], + signal_signature: str, + args_names: Sequence[str], + flags: int, + original_method: FunctionType + ): + super().__init__( + signal_name, + signal_signature, + args_names, + flags, + original_method, + ) + + self.local_callbacks: WeakSet[Callable[[T], Any]] = WeakSet() + def __get__( self, obj: Optional[DbusInterfaceBaseAsync], @@ -162,29 +181,17 @@ def __init__( self.__doc__ = dbus_signal.__doc__ async def catch(self) -> AsyncIterator[T]: - signal_key = ( - self.dbus_signal.interface_name, - self.dbus_signal.signal_name, - ) - - try: - list_of_queues = self.local_meta.local_signal_queues[ - signal_key - ] - except KeyError: - list_of_queues = set() - self.local_meta.local_signal_queues[ - signal_key] = list_of_queues - new_queue: Queue[T] = Queue() - list_of_queues.add(new_queue) + signal_callbacks = self.dbus_signal.local_callbacks try: + put_method = new_queue.put_nowait + signal_callbacks.add(put_method) while True: next_data = await new_queue.get() yield next_data finally: - list_of_queues.remove(new_queue) + signal_callbacks.remove(put_method) __aiter__ = catch @@ -227,19 +234,8 @@ def _emit_dbus_signal(self, args: T) -> None: def emit(self, args: T) -> None: self._emit_dbus_signal(args) - signal_key = ( - self.dbus_signal.interface_name, - self.dbus_signal.signal_name, - ) - - try: - list_of_queues = self.local_meta.local_signal_queues[ - signal_key] - except KeyError: - return - - for local_queue in list_of_queues: - local_queue.put_nowait(args) + for callback in self.dbus_signal.local_callbacks: + callback(args) class DbusSignalAsyncClassBind(DbusSignalAsyncBaseBind[T]): From 6010f265e566e25894435b4f671b92dade0a8ed2 Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sun, 4 Feb 2024 21:38:09 +0600 Subject: [PATCH 064/188] Add IsolatedDbusTestCase.assertDbusSignalEmits When used in the `async with` block it will assert that the signal gets emitted at least once. The with block returns the DbusSignalRecorder object which can be used to test the data emitted by signal. API is not final and is not documented for now. It should be finalized by the time 0.12.0 version releases. --- src/sdbus/dbus_proxy_async_signal.py | 20 +++- src/sdbus/unittest.py | 148 ++++++++++++++++++++++++++- test/test_sdbus_async.py | 60 ++++++----- 3 files changed, 194 insertions(+), 34 deletions(-) diff --git a/src/sdbus/dbus_proxy_async_signal.py b/src/sdbus/dbus_proxy_async_signal.py index 8c55583..291bfc7 100644 --- a/src/sdbus/dbus_proxy_async_signal.py +++ b/src/sdbus/dbus_proxy_async_signal.py @@ -45,7 +45,7 @@ from typing import Any, Callable, Optional, Sequence, Tuple, Type from .dbus_proxy_async_interface_base import DbusInterfaceBaseAsync - from .sd_bus_internals import SdBus, SdBusMessage + from .sd_bus_internals import SdBus, SdBusMessage, SdBusSlot T = TypeVar('T') @@ -116,14 +116,24 @@ def __init__( self.__doc__ = dbus_signal.__doc__ - async def catch(self) -> AsyncIterator[T]: - message_queue: Queue[SdBusMessage] = Queue() - - match_slot = await self.proxy_meta.attached_bus.match_signal_async( + async def _register_match_slot( + self, + bus: SdBus, + callback: Callable[[SdBusMessage], Any], + ) -> SdBusSlot: + return await bus.match_signal_async( self.proxy_meta.service_name, self.proxy_meta.object_path, self.dbus_signal.interface_name, self.dbus_signal.signal_name, + callback, + ) + + async def catch(self) -> AsyncIterator[T]: + message_queue: Queue[SdBusMessage] = Queue() + + match_slot = await self._register_match_slot( + self.proxy_meta.attached_bus, message_queue.put_nowait, ) diff --git a/src/sdbus/unittest.py b/src/sdbus/unittest.py index e90698a..5fc5245 100644 --- a/src/sdbus/unittest.py +++ b/src/sdbus/unittest.py @@ -19,6 +19,7 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA from __future__ import annotations +from asyncio import Event, TimeoutError, wait_for from os import environ, kill from pathlib import Path from signal import SIGTERM @@ -27,11 +28,33 @@ from tempfile import TemporaryDirectory from typing import TYPE_CHECKING from unittest import IsolatedAsyncioTestCase +from weakref import ref as weak_ref -from sdbus import sd_bus_open_user, set_default_bus +from .dbus_common_funcs import set_default_bus +from .dbus_proxy_async_signal import ( + DbusSignalAsyncLocalBind, + DbusSignalAsyncProxyBind, +) +from .sd_bus_internals import SdBusMessage, sd_bus_open_user if TYPE_CHECKING: - from typing import ClassVar + from typing import ( + Any, + AsyncContextManager, + ClassVar, + List, + Optional, + TypeVar, + Union, + ) + + from .dbus_proxy_async_signal import ( + DbusSignalAsync, + DbusSignalAsyncBaseBind, + ) + from .sd_bus_internals import SdBus, SdBusSlot + + T = TypeVar('T') dbus_config = ''' @@ -49,6 +72,114 @@ ''' +class DbusSignalRecorderBase: + def __init__( + self, + testcase: IsolatedDbusTestCase, + timeout: Union[int, float], + ): + self._testcase = testcase + self._timeout = timeout + self._captured_data: List[Any] = [] + self._ready_event = Event() + self._callback_method = self._callback + + async def start(self) -> None: + raise NotImplementedError + + async def stop(self) -> None: + raise NotImplementedError + + async def __aenter__(self) -> DbusSignalRecorderBase: + raise NotImplementedError + + async def __aexit__( + self, + exc_type: Any, + exc_value: Any, + traceback: Any, + ) -> None: + if exc_type is not None: + return + + try: + await wait_for(self._ready_event.wait(), timeout=self._timeout) + except TimeoutError: + raise AssertionError("D-Bus signal not captured.") from None + + def _callback(self, data: Any) -> None: + if isinstance(data, SdBusMessage): + data = data.get_contents() + + self._captured_data.append(data) + self._ready_event.set() + + def assert_emitted_once_with(self, data: Any) -> None: + captured_signals_num = len(self._captured_data) + if captured_signals_num != 1: + raise AssertionError( + f"Expected one captured signal got {captured_signals_num}" + ) + + self._testcase.assertEqual(self._captured_data[0], data) + + +class DbusSignalRecorderRemote(DbusSignalRecorderBase): + def __init__( + self, + testcase: IsolatedDbusTestCase, + timeout: Union[int, float], + bus: SdBus, + remote_signal: DbusSignalAsyncProxyBind[Any], + ): + super().__init__(testcase, timeout) + self._bus = bus + self._match_slot: Optional[SdBusSlot] = None + self._remote_signal = remote_signal + + async def __aenter__(self) -> DbusSignalRecorderBase: + self._match_slot = await self._remote_signal._register_match_slot( + self._bus, + self._callback_method, + ) + + return self + + async def __aexit__( + self, + exc_type: Any, + exc_value: Any, + traceback: Any, + ) -> None: + try: + await super().__aexit__(exc_type, exc_value, traceback) + finally: + if self._match_slot is not None: + self._match_slot.close() + + +class DbusSignalRecorderLocal(DbusSignalRecorderBase): + def __init__( + self, + testcase: IsolatedDbusTestCase, + timeout: Union[int, float], + local_signal: DbusSignalAsyncLocalBind[Any], + ): + super().__init__(testcase, timeout) + self._local_signal_ref: weak_ref[DbusSignalAsync[Any]] = ( + weak_ref(local_signal.dbus_signal) + ) + + async def __aenter__(self) -> DbusSignalRecorderBase: + local_signal = self._local_signal_ref() + + if local_signal is None: + raise RuntimeError + + local_signal.local_callbacks.add(self._callback_method) + return self + + class IsolatedDbusTestCase(IsolatedAsyncioTestCase): dbus_executable_name: ClassVar[str] = 'dbus-daemon' @@ -95,3 +226,16 @@ def tearDown(self) -> None: environ.pop('DBUS_SESSION_BUS_ADDRESS') if self.old_session_bus_address is not None: environ['DBUS_SESSION_BUS_ADDRESS'] = self.old_session_bus_address + + def assertDbusSignalEmits( + self, + signal: DbusSignalAsyncBaseBind[Any], + timeout: Union[int, float] = 1, + ) -> AsyncContextManager[DbusSignalRecorderBase]: + + if isinstance(signal, DbusSignalAsyncLocalBind): + return DbusSignalRecorderLocal(self, timeout, signal) + elif isinstance(signal, DbusSignalAsyncProxyBind): + return DbusSignalRecorderRemote(self, timeout, self.bus, signal) + else: + raise TypeError("Unknown or unsupported signal class.") diff --git a/test/test_sdbus_async.py b/test/test_sdbus_async.py index 9a04228..43485d9 100644 --- a/test/test_sdbus_async.py +++ b/test/test_sdbus_async.py @@ -19,7 +19,7 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA from __future__ import annotations -from asyncio import Event, get_running_loop, sleep, wait, wait_for +from asyncio import Event, get_running_loop, sleep, wait_for from asyncio.subprocess import create_subprocess_exec from typing import TYPE_CHECKING, cast from unittest import SkipTest @@ -51,8 +51,7 @@ ) if TYPE_CHECKING: - from asyncio import Task - from typing import Any, Tuple + from typing import Tuple from sdbus.dbus_proxy_async_interfaces import ( DBUS_PROPERTIES_CHANGED_TYPING, @@ -430,21 +429,30 @@ async def test_properties(self) -> None: async def test_signal(self) -> None: test_object, test_object_connection = initialize_object() - loop = get_running_loop() - test_tuple = ('sgfsretg', 'asd') - aiter_dbus: Any = test_object_connection.test_signal.__aiter__() - anext_dbus: Task[Any] = loop.create_task(aiter_dbus.__anext__()) - aiter_local: Any = test_object.test_signal.__aiter__() - anext_local: Task[Any] = loop.create_task(aiter_local.__anext__()) - - loop.call_later(0.1, test_object.test_signal.emit, test_tuple) + async with ( + self.assertDbusSignalEmits( + test_object.test_signal + ) as local_signals_record, + self.assertDbusSignalEmits( + test_object_connection.test_signal + ) as remote_signals_record + ): + test_object.test_signal.emit(test_tuple) - await wait((anext_dbus, anext_local), timeout=1) + async with ( + self.assertDbusSignalEmits( + test_object.test_signal + ) as local_signals_record, + self.assertDbusSignalEmits( + test_object_connection.test_signal + ) as remote_signals_record + ): + test_object.test_signal.emit(test_tuple) - self.assertEqual(test_tuple, anext_dbus.result()) - self.assertEqual(test_tuple, anext_local.result()) + local_signals_record.assert_emitted_once_with(test_tuple) + remote_signals_record.assert_emitted_once_with(test_tuple) async def test_signal_catch_anywhere(self) -> None: test_object, test_object_connection = initialize_object() @@ -731,20 +739,18 @@ async def test_properties_get_all_dict(self) -> None: async def test_empty_signal(self) -> None: test_object, test_object_connection = initialize_object() - loop = get_running_loop() - - aiter_dbus: Any = test_object_connection.empty_signal.__aiter__() - anext_dbus: Task[Any] = loop.create_task(aiter_dbus.__anext__()) - aiter_local: Any = test_object.empty_signal.__aiter__() - anext_local: Task[Any] = loop.create_task(aiter_local.__anext__()) - - loop.call_later(0.1, test_object.empty_signal.emit, None) - - await wait((anext_dbus, anext_local), timeout=1) - - self.assertIsNone(anext_dbus.result()) + async with ( + self.assertDbusSignalEmits( + test_object.empty_signal + ) as local_signals_record, + self.assertDbusSignalEmits( + test_object_connection.empty_signal + ) as remote_signals_record + ): + test_object.empty_signal.emit(None) - self.assertIsNone(anext_local.result()) + local_signals_record.assert_emitted_once_with(None) + remote_signals_record.assert_emitted_once_with(None) async def test_properties_changed(self) -> None: test_object, test_object_connection = initialize_object() From 60bd82c72b7eeb2d706fe60a4b299b53da368d73 Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sun, 4 Feb 2024 21:51:00 +0600 Subject: [PATCH 065/188] tests: Do not use Parenthesized context managers It was added in Python 3.10 so trying to run the tests on earlier version raises SynxtaxError. --- test/test_sdbus_async.py | 27 +++++++++------------------ 1 file changed, 9 insertions(+), 18 deletions(-) diff --git a/test/test_sdbus_async.py b/test/test_sdbus_async.py index 43485d9..6a4cb58 100644 --- a/test/test_sdbus_async.py +++ b/test/test_sdbus_async.py @@ -431,24 +431,18 @@ async def test_signal(self) -> None: test_tuple = ('sgfsretg', 'asd') - async with ( - self.assertDbusSignalEmits( + async with self.assertDbusSignalEmits( test_object.test_signal - ) as local_signals_record, - self.assertDbusSignalEmits( + ) as local_signals_record, self.assertDbusSignalEmits( test_object_connection.test_signal - ) as remote_signals_record - ): + ) as remote_signals_record: test_object.test_signal.emit(test_tuple) - async with ( - self.assertDbusSignalEmits( + async with self.assertDbusSignalEmits( test_object.test_signal - ) as local_signals_record, - self.assertDbusSignalEmits( + ) as local_signals_record, self.assertDbusSignalEmits( test_object_connection.test_signal - ) as remote_signals_record - ): + ) as remote_signals_record: test_object.test_signal.emit(test_tuple) local_signals_record.assert_emitted_once_with(test_tuple) @@ -739,14 +733,11 @@ async def test_properties_get_all_dict(self) -> None: async def test_empty_signal(self) -> None: test_object, test_object_connection = initialize_object() - async with ( - self.assertDbusSignalEmits( + async with self.assertDbusSignalEmits( test_object.empty_signal - ) as local_signals_record, - self.assertDbusSignalEmits( + ) as local_signals_record, self.assertDbusSignalEmits( test_object_connection.empty_signal - ) as remote_signals_record - ): + ) as remote_signals_record: test_object.empty_signal.emit(None) local_signals_record.assert_emitted_once_with(None) From 540696b5ee6a2aef9dc3aaa2e74591a30b437c7a Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sun, 4 Feb 2024 21:59:57 +0600 Subject: [PATCH 066/188] Add license section to README It explains that the main license is LGPL-2.1-or-later and that both GPL and LGPL texts are present as LGPL is an extension of GPL. --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 707135e..f7c3c0a 100644 --- a/README.md +++ b/README.md @@ -207,3 +207,9 @@ task_hello_world = loop.create_task(get_hello_world()) loop.run_forever() ``` + +## License + +Python-sdbus is licensed under [LGPL-2.1-or-later](https://spdx.org/licenses/LGPL-2.1-or-later.html). + +The LGPL license is an extension of GPL license therefore both licenses' texts are required. From e7f3bb115ec08790cfdba5f35bcd9178d1eecc15 Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sat, 10 Feb 2024 17:18:33 +0600 Subject: [PATCH 067/188] Add SdBus.method_call_timeout_usec property Controls the D-Bus method call timeout. Is in microseconds as that is what sd-bus uses. --- src/sdbus/sd_bus_internals.py | 1 + src/sdbus/sd_bus_internals_bus.c | 23 +++++++++++++++++++++++ test/test_low_level_api.py | 21 +++++++++++++++++++-- 3 files changed, 43 insertions(+), 2 deletions(-) diff --git a/src/sdbus/sd_bus_internals.py b/src/sdbus/sd_bus_internals.py index 9bf21cb..8337e2f 100644 --- a/src/sdbus/sd_bus_internals.py +++ b/src/sdbus/sd_bus_internals.py @@ -222,6 +222,7 @@ def start(self) -> None: raise NotImplementedError(__STUB_ERROR) address: Optional[str] = None + method_call_timeout_usec: int = 0 def sd_bus_open() -> SdBus: diff --git a/src/sdbus/sd_bus_internals_bus.c b/src/sdbus/sd_bus_internals_bus.c index 819159f..4902597 100644 --- a/src/sdbus/sd_bus_internals_bus.c +++ b/src/sdbus/sd_bus_internals_bus.c @@ -669,8 +669,31 @@ static PyObject* SdBus_address_getter(SdBusObject* self, void* Py_UNUSED(closure return PyUnicode_FromString(bus_address); } +static PyObject* SdBus_method_call_timeout_usec_getter(SdBusObject* self, void* Py_UNUSED(closure)) { + uint64_t timeout_usec = 0; + CALL_SD_BUS_AND_CHECK(sd_bus_get_method_call_timeout(self->sd_bus_ref, &timeout_usec)); + + return PyLong_FromUnsignedLongLong((unsigned long long)timeout_usec); +} + +static int SdBus_method_call_timeout_usec_setter(SdBusObject* self, PyObject* new_value, void* Py_UNUSED(closure)) { + if (NULL == new_value) { + PyErr_SetString(PyExc_ValueError, "Cannot delete method call timeout value"); + return -1; + } + + unsigned long long new_timeout_usec = PyLong_AsUnsignedLongLong(new_value); + if ((((unsigned long long)-1) == new_timeout_usec) && (PyErr_Occurred() != NULL)) { + return -1; + } + CALL_SD_BUS_CHECK_RETURN_NEG1(sd_bus_set_method_call_timeout(self->sd_bus_ref, (uint64_t)new_timeout_usec)); + return 0; +} + static PyGetSetDef SdBus_properies[] = { {"address", (getter)SdBus_address_getter, NULL, PyDoc_STR("Bus address."), NULL}, + {"method_call_timeout_usec", (getter)SdBus_method_call_timeout_usec_getter, (setter)SdBus_method_call_timeout_usec_setter, + PyDoc_STR("D-Bus call timeout in microseconds."), NULL}, {0}, }; diff --git a/test/test_low_level_api.py b/test/test_low_level_api.py index 62b7f33..1a19a5a 100644 --- a/test/test_low_level_api.py +++ b/test/test_low_level_api.py @@ -19,7 +19,7 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA from __future__ import annotations -from unittest import SkipTest, main +from unittest import SkipTest, TestCase, main from sdbus.sd_bus_internals import ( SdBus, @@ -31,13 +31,15 @@ from sdbus.unittest import IsolatedDbusTestCase -class TestDbusTypes(IsolatedDbusTestCase): +class TestInitDbus(IsolatedDbusTestCase): def test_init_bus(self) -> None: not_connected_bus = SdBus() self.assertIsNone(not_connected_bus.address) self.assertIsNotNone(self.bus.address) + +class TestLowLeveApi(TestCase): def test_validation_funcs(self) -> None: try: self.assertTrue( @@ -79,6 +81,21 @@ def test_validation_funcs(self) -> None: ) ) + def test_bus_method_call_timeout(self) -> None: + bus = SdBus() + + self.assertIsNotNone(bus.method_call_timeout_usec) + + test_timeout_usec = 10 * 10**6 # 10 seconds + bus.method_call_timeout_usec = test_timeout_usec + self.assertEqual(test_timeout_usec, bus.method_call_timeout_usec) + + with self.assertRaises(TypeError): + bus.method_call_timeout_usec = "test" # type: ignore + + with self.assertRaises(ValueError): + del bus.method_call_timeout_usec + if __name__ == "__main__": main() From c0b2965fc2280019f5e4f3a0e357089e5c6cc7af Mon Sep 17 00:00:00 2001 From: igo95862 Date: Mon, 12 Feb 2024 00:55:18 +0600 Subject: [PATCH 068/188] test: Add a test that methods properly return None Some error in implementation might make methods return something other than None when they supposed to. --- test/test_sdbus_async.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/test/test_sdbus_async.py b/test/test_sdbus_async.py index 6a4cb58..a38ed33 100644 --- a/test/test_sdbus_async.py +++ b/test/test_sdbus_async.py @@ -219,6 +219,10 @@ async def looong_method(self) -> None: def empty_signal(self) -> None: raise NotImplementedError + @dbus_method_async() + async def returns_none_method(self) -> None: + return + class DbusErrorTest(DbusFailedError): dbus_error_name = 'org.example.Error' @@ -310,6 +314,17 @@ async def test_method(self) -> None: self.assertTrue(await test_object_connection.get_sender()) + with self.subTest("Test method that returns None"): + self.assertIsNone( + + await test_object + .returns_none_method() # type: ignore[func-returns-value] + ) + self.assertIsNone( + await test_object_connection + .returns_none_method() # type: ignore[func-returns-value] + ) + async def test_subclass(self) -> None: test_object, test_object_connection = initialize_object() From 8bc27ea927e82211dd70c10601ec6c96bccc3910 Mon Sep 17 00:00:00 2001 From: Adrien Cossa Date: Wed, 14 Feb 2024 12:37:40 +0100 Subject: [PATCH 069/188] Fix typos ("singal" --> "signal") --- src/sdbus/dbus_common_elements.py | 2 +- src/sdbus/dbus_proxy_async_signal.py | 4 ++-- test/test_sdbus_async.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/sdbus/dbus_common_elements.py b/src/sdbus/dbus_common_elements.py index 1ec287d..42e1efd 100644 --- a/src/sdbus/dbus_common_elements.py +++ b/src/sdbus/dbus_common_elements.py @@ -261,7 +261,7 @@ def __init__(self, self.flags = flags -class DbusSingalCommon(DbusSomethingCommon): +class DbusSignalCommon(DbusSomethingCommon): def __init__(self, signal_name: Optional[str], signal_signature: str, diff --git a/src/sdbus/dbus_proxy_async_signal.py b/src/sdbus/dbus_proxy_async_signal.py index 291bfc7..39a2329 100644 --- a/src/sdbus/dbus_proxy_async_signal.py +++ b/src/sdbus/dbus_proxy_async_signal.py @@ -36,7 +36,7 @@ DbusBindedAsync, DbusLocalObjectMeta, DbusRemoteObjectMeta, - DbusSingalCommon, + DbusSignalCommon, DbusSomethingAsync, ) from .dbus_common_funcs import get_default_bus @@ -51,7 +51,7 @@ T = TypeVar('T') -class DbusSignalAsync(DbusSomethingAsync, DbusSingalCommon, Generic[T]): +class DbusSignalAsync(DbusSomethingAsync, DbusSignalCommon, Generic[T]): def __init__( self, diff --git a/test/test_sdbus_async.py b/test/test_sdbus_async.py index a38ed33..1908cd9 100644 --- a/test/test_sdbus_async.py +++ b/test/test_sdbus_async.py @@ -684,7 +684,7 @@ async def too_long_wait() -> None: with self.assertRaises(SdBusLibraryError): await wait_for(too_long_wait(), timeout=1) - async def test_singal_queue_wildcard_match(self) -> None: + async def test_signal_queue_wildcard_match(self) -> None: test_object, test_object_connection = initialize_object() loop = get_running_loop() From 3535e7dacc4af72f3af3bc11ce95f93fd1fbbb1d Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sun, 18 Feb 2024 16:04:46 +0600 Subject: [PATCH 070/188] test: Add type hints tests These new functions are not intended to be called but instead the type checker in CI will verify that the types are properly hinted. --- test/test_typing.py | 147 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 147 insertions(+) create mode 100644 test/test_typing.py diff --git a/test/test_typing.py b/test/test_typing.py new file mode 100644 index 0000000..0197eb4 --- /dev/null +++ b/test/test_typing.py @@ -0,0 +1,147 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later + +# Copyright (C) 2024 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 typing import TYPE_CHECKING + +from sdbus import ( + DbusInterfaceCommon, + DbusInterfaceCommonAsync, + dbus_method, + dbus_method_async, + dbus_property, + dbus_property_async, + dbus_signal_async, +) + +if TYPE_CHECKING: + from typing import List + + +class TestTypingBlocking( + DbusInterfaceCommon, + interface_name="example.com", +): + + @dbus_method(result_signature="as") + def get_str_list_method(self) -> List[str]: + raise NotImplementedError + + @dbus_property("as") + def str_list_property(self) -> List[str]: + raise NotImplementedError + + +# These functions are not meant to be executed +# but exist to be type checked. + +def check_blocking_interface_method_typing( + test_interface: TestTypingBlocking, +) -> None: + + should_be_list = test_interface.get_str_list_method() + should_be_list.append("test") + + for x in should_be_list: + x.capitalize() + + +def check_blocking_interface_property_typing( + test_interface: TestTypingBlocking, +) -> None: + + should_be_list = test_interface.str_list_property + should_be_list.append("test") + + for x in should_be_list: + x.capitalize() + + test_interface.str_list_property = ["test", "foobar"] + + +class TestTypingAsync( + DbusInterfaceCommonAsync, + interface_name="example.com", +): + + @dbus_method_async(result_signature="as") + async def get_str_list_method(self) -> List[str]: + raise NotImplementedError + + @dbus_property_async("as") + def str_list_property(self) -> List[str]: + raise NotImplementedError + + @dbus_signal_async("as") + def str_list_signal(self) -> List[str]: + raise NotImplementedError + + +async def check_async_interface_method_typing( + test_interface: TestTypingAsync, +) -> None: + + should_be_list = await test_interface.get_str_list_method() + should_be_list.append("test") + + for x in should_be_list: + x.capitalize() + + +async def check_async_interface_property_typing( + test_interface: TestTypingAsync, +) -> None: + + should_be_list = await test_interface.str_list_property + should_be_list.append("test") + + for x in should_be_list: + x.capitalize() + + should_be_list2 = await test_interface.str_list_property.get_async() + should_be_list2.append("test") + + for x in should_be_list2: + x.capitalize() + + await test_interface.str_list_property.set_async(["test", "foobar"]) + + +async def check_async_interface_signal_typing( + test_interface: TestTypingAsync, +) -> None: + + async for ls in test_interface.str_list_signal: + ls.append("test") + for x in ls: + x.capitalize() + + async for ls2 in test_interface.str_list_signal.catch(): + ls2.append("test") + for x2 in ls2: + x2.capitalize() + + async for p, ls3 in test_interface.str_list_signal.catch_anywhere(): + p.capitalize() + ls3.append("test") + for x3 in ls3: + x3.capitalize() + + test_interface.str_list_signal.emit(["test", "foobar"]) From f58ace9ef9d57080dd03c47560cf81bb5b502fce Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sun, 18 Feb 2024 16:44:06 +0600 Subject: [PATCH 071/188] Add SdBusMessage.parse_to_tuple() method Parses the message data to a tuple. The main difference is handling of no data and a single complete type messages. When message has no data returns a zero size tuple. When message has a single complete type return a tuple of one element. This makes it simpler to implement the D-Bus -> Python calls because now all it takes is calling Python function with unpacked tuple. Unpacking zero size tuple is equivalent to calling function with no arguments. This also fixes methods that take a single struct. Before there was ambiguity if a method was called with a struct or multiple complete types. Now a single struct would be a part of one element tuple. --- src/sdbus/dbus_proxy_async_method.py | 8 +------- src/sdbus/sd_bus_internals.py | 3 +++ src/sdbus/sd_bus_internals_message.c | 24 ++++++++++++++++++++++++ test/test_sdbus_async.py | 22 +++++++++++++++++++++- 4 files changed, 49 insertions(+), 8 deletions(-) diff --git a/src/sdbus/dbus_proxy_async_method.py b/src/sdbus/dbus_proxy_async_method.py index ffd41ee..f7d39c1 100644 --- a/src/sdbus/dbus_proxy_async_method.py +++ b/src/sdbus/dbus_proxy_async_method.py @@ -150,19 +150,13 @@ async def _dbus_reply_call_method( request_message: SdBusMessage, local_object: DbusInterfaceBaseAsync, ) -> Any: - request_data = request_message.get_contents() local_method = self.dbus_method.original_method.__get__( local_object, None) CURRENT_MESSAGE.set(request_message) - if isinstance(request_data, tuple): - return await local_method(*request_data) - elif request_data is None: - return await local_method() - else: - return await local_method(request_data) + return await local_method(*request_message.parse_to_tuple()) async def _dbus_reply_call( self, diff --git a/src/sdbus/sd_bus_internals.py b/src/sdbus/sd_bus_internals.py index 8337e2f..e9ee1bb 100644 --- a/src/sdbus/sd_bus_internals.py +++ b/src/sdbus/sd_bus_internals.py @@ -136,6 +136,9 @@ def create_error_reply( def send(self) -> None: raise NotImplementedError(__STUB_ERROR) + def parse_to_tuple(self) -> Tuple[Any, ...]: + raise NotImplementedError(__STUB_ERROR) + expect_reply: bool = False destination: Optional[str] = None path: Optional[str] = None diff --git a/src/sdbus/sd_bus_internals_message.c b/src/sdbus/sd_bus_internals_message.c index 85758db..f5631b3 100644 --- a/src/sdbus/sd_bus_internals_message.c +++ b/src/sdbus/sd_bus_internals_message.c @@ -980,6 +980,29 @@ static PyObject* SdBusMessage_get_contents2(SdBusMessageObject* self, PyObject* return iter_tuple_or_single(&read_parser); } +static PyObject* SdBusMessage_parse_to_tuple(SdBusMessageObject* self, PyObject* Py_UNUSED(args)) { + const char* message_signature = sd_bus_message_get_signature(self->message_ref, 0); + + if (message_signature == NULL) { + PyErr_SetString(PyExc_ValueError, "Failed to get message signature."); + return NULL; + } + if (message_signature[0] == '\0') { + // Empty message. Return zero size tuple. + return PyTuple_New(0); + } + + CALL_SD_BUS_AND_CHECK(sd_bus_message_rewind(self->message_ref, 0)); + _Parse_state read_parser = { + .message = self->message_ref, + .container_char_ptr = message_signature, + .index = 0, + .max_index = strlen(message_signature), + }; + + return _iter_struct(&read_parser); +} + #ifndef Py_LIMITED_API static SdBusMessageObject* SdBusMessage_create_error_reply(SdBusMessageObject* self, PyObject* const* args, Py_ssize_t nargs) { SD_BUS_PY_CHECK_ARGS_NUMBER(2); @@ -1012,6 +1035,7 @@ static PyMethodDef SdBusMessage_methods[] = { {"dump", (PyCFunction)SdBusMessage_dump, METH_NOARGS, PyDoc_STR("Dump message to stdout.")}, {"seal", (PyCFunction)SdBusMessage_seal, METH_NOARGS, PyDoc_STR("Seal message contents.")}, {"get_contents", (PyCFunction)SdBusMessage_get_contents2, METH_NOARGS, PyDoc_STR("Iterate over message contents.")}, + {"parse_to_tuple", (PyCFunction)SdBusMessage_parse_to_tuple, METH_NOARGS, PyDoc_STR("Parse message data to a tuple.")}, {"create_reply", (PyCFunction)SdBusMessage_create_reply, METH_NOARGS, PyDoc_STR("Create reply message.")}, {"create_error_reply", (SD_BUS_PY_FUNC_TYPE)SdBusMessage_create_error_reply, SD_BUS_PY_METH, PyDoc_STR("Create error reply with error name and error message.")}, diff --git a/test/test_sdbus_async.py b/test/test_sdbus_async.py index 1908cd9..e8be301 100644 --- a/test/test_sdbus_async.py +++ b/test/test_sdbus_async.py @@ -223,6 +223,17 @@ def empty_signal(self) -> None: async def returns_none_method(self) -> None: return + @dbus_method_async( + input_signature="(iiii)", + result_signature="i", + ) + async def takes_struct_method( + self, + int_struct: Tuple[int, int, int, int], + ) -> int: + a, b, c, d = int_struct + return a*b*c*d + class DbusErrorTest(DbusFailedError): dbus_error_name = 'org.example.Error' @@ -316,7 +327,6 @@ async def test_method(self) -> None: with self.subTest("Test method that returns None"): self.assertIsNone( - await test_object .returns_none_method() # type: ignore[func-returns-value] ) @@ -325,6 +335,16 @@ async def test_method(self) -> None: .returns_none_method() # type: ignore[func-returns-value] ) + with self.subTest("Test method that takes a single struct"): + self.assertEqual( + await test_object.takes_struct_method((2, 3, 4, 5)), + 120, + ) + self.assertEqual( + await test_object_connection.takes_struct_method((9, 8, 7, 6)), + 3024, + ) + async def test_subclass(self) -> None: test_object, test_object_connection = initialize_object() From 899a7dbcb92fecaede4f02741fd1a85e44ebbcd1 Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sun, 18 Feb 2024 21:17:34 +0600 Subject: [PATCH 072/188] Fix SdBusSlot_close having too few arguments The second argument must be present even if it is not used to be able to cast the function to `PyCFunction`. --- src/sdbus/sd_bus_internals.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sdbus/sd_bus_internals.c b/src/sdbus/sd_bus_internals.c index 55965a5..c96518e 100644 --- a/src/sdbus/sd_bus_internals.c +++ b/src/sdbus/sd_bus_internals.c @@ -54,7 +54,7 @@ static void SdBusSlot_dealloc(SdBusSlotObject* self) { SD_BUS_DEALLOC_TAIL; } -static PyObject* SdBusSlot_close(SdBusSlotObject* self) { +static PyObject* SdBusSlot_close(SdBusSlotObject* self, PyObject* Py_UNUSED(args)) { sd_bus_slot_unref(self->slot_ref); self->slot_ref = NULL; From 03593a13167a5a0bd6818489d63f99777ab76299 Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sat, 24 Feb 2024 19:49:27 +0600 Subject: [PATCH 073/188] Fix sending extremely large messages getting stuck when using asyncio Apparently the file descriptor returned by `sd_bus_get_fd` must be monitored for both reading and writing depending on `sd_bus_get_events` return value. This comes in to play when an extremely large message needs to be sent over D-Bus. Such extremely large message will need multiple `sendmsg` calls with file descriptor being monitored to when the write processing of the message has to be done. This commit will add or remove asyncio loop file descriptor watchers based on changes to `sd_bus_get_events` value. --- src/sdbus/sd_bus_internals.c | 4 ++ src/sdbus/sd_bus_internals.h | 5 +- src/sdbus/sd_bus_internals.py | 2 +- src/sdbus/sd_bus_internals_bus.c | 85 +++++++++++++++++++------------- test/test_sdbus_async.py | 25 ++++++++++ 5 files changed, 86 insertions(+), 35 deletions(-) diff --git a/src/sdbus/sd_bus_internals.c b/src/sdbus/sd_bus_internals.c index c96518e..b97f444 100644 --- a/src/sdbus/sd_bus_internals.c +++ b/src/sdbus/sd_bus_internals.c @@ -28,6 +28,8 @@ PyObject* set_result_str = NULL; PyObject* set_exception_str = NULL; PyObject* add_reader_str = NULL; PyObject* remove_reader_str = NULL; +PyObject* add_writer_str = NULL; +PyObject* remove_writer_str = NULL; PyObject* empty_str = NULL; PyObject* null_str = NULL; PyObject* extend_str = NULL; @@ -171,6 +173,8 @@ PyMODINIT_FUNC PyInit_sd_bus_internals(void) { create_task_str = CALL_PYTHON_AND_CHECK(PyUnicode_FromString("create_task")); remove_reader_str = CALL_PYTHON_AND_CHECK(PyUnicode_FromString("remove_reader")); add_reader_str = CALL_PYTHON_AND_CHECK(PyUnicode_FromString("add_reader")); + add_writer_str = CALL_PYTHON_AND_CHECK(PyUnicode_FromString("add_writer")); + remove_writer_str = CALL_PYTHON_AND_CHECK(PyUnicode_FromString("remove_writer")); empty_str = CALL_PYTHON_AND_CHECK(PyUnicode_FromString("")); null_str = CALL_PYTHON_AND_CHECK(PyUnicode_FromStringAndSize("\0", 1)); extend_str = CALL_PYTHON_AND_CHECK(PyUnicode_FromString("extend")); diff --git a/src/sdbus/sd_bus_internals.h b/src/sdbus/sd_bus_internals.h index 0e90399..2694351 100644 --- a/src/sdbus/sd_bus_internals.h +++ b/src/sdbus/sd_bus_internals.h @@ -246,6 +246,8 @@ extern PyObject* set_result_str; extern PyObject* set_exception_str; extern PyObject* add_reader_str; extern PyObject* remove_reader_str; +extern PyObject* add_writer_str; +extern PyObject* remove_writer_str; extern PyObject* empty_str; extern PyObject* null_str; extern PyObject* extend_str; @@ -330,7 +332,8 @@ extern PyObject* SdBusMessage_class; typedef struct { PyObject_HEAD; sd_bus* sd_bus_ref; - PyObject* reader_fd; + PyObject* bus_fd; + int asyncio_watchers_last_state; } SdBusObject; extern PyType_Spec SdBusType; diff --git a/src/sdbus/sd_bus_internals.py b/src/sdbus/sd_bus_internals.py index e9ee1bb..594647b 100644 --- a/src/sdbus/sd_bus_internals.py +++ b/src/sdbus/sd_bus_internals.py @@ -156,7 +156,7 @@ def call_async( /) -> Future[SdBusMessage]: raise NotImplementedError(__STUB_ERROR) - def drive(self) -> None: + def process(self) -> None: raise NotImplementedError(__STUB_ERROR) def get_fd(self) -> int: diff --git a/src/sdbus/sd_bus_internals_bus.c b/src/sdbus/sd_bus_internals_bus.c index 4902597..759a914 100644 --- a/src/sdbus/sd_bus_internals_bus.c +++ b/src/sdbus/sd_bus_internals_bus.c @@ -19,11 +19,12 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #include +#include #include "sd_bus_internals.h" static void SdBus_dealloc(SdBusObject* self) { sd_bus_unref(self->sd_bus_ref); - Py_XDECREF(self->reader_fd); + Py_XDECREF(self->bus_fd); SD_BUS_DEALLOC_TAIL; } @@ -229,46 +230,24 @@ int future_set_exception_from_message(PyObject* future, sd_bus_message* message) return 0; } -static PyObject* SdBus_drive(SdBusObject* self, PyObject* Py_UNUSED(args)); - static PyObject* SdBus_get_fd(SdBusObject* self, PyObject* Py_UNUSED(args)) { int file_descriptor = CALL_SD_BUS_AND_CHECK(sd_bus_get_fd(self->sd_bus_ref)); return PyLong_FromLong((long)file_descriptor); } -#define CHECK_SD_BUS_READER \ - ({ \ - if (self->reader_fd == NULL) { \ - CALL_PYTHON_EXPECT_NONE(register_reader(self)); \ - } \ - }) - -PyObject* register_reader(SdBusObject* self) { - PyObject* running_loop CLEANUP_PY_OBJECT = CALL_PYTHON_AND_CHECK(PyObject_CallFunctionObjArgs(asyncio_get_running_loop, NULL)); - PyObject* new_reader_fd CLEANUP_PY_OBJECT = CALL_PYTHON_AND_CHECK(SdBus_get_fd(self, NULL)); - PyObject* drive_method CLEANUP_PY_OBJECT = CALL_PYTHON_AND_CHECK(PyObject_GetAttrString((PyObject*)self, "drive")); - Py_XDECREF(CALL_PYTHON_AND_CHECK(PyObject_CallMethodObjArgs(running_loop, add_reader_str, new_reader_fd, drive_method, NULL))); - Py_INCREF(new_reader_fd); - self->reader_fd = new_reader_fd; - Py_RETURN_NONE; -} +static PyObject* SdBus_asyncio_update_fd_watchers(SdBusObject* self); -PyObject* unregister_reader(SdBusObject* self) { - PyObject* running_loop CLEANUP_PY_OBJECT = CALL_PYTHON_AND_CHECK(PyObject_CallFunctionObjArgs(asyncio_get_running_loop, NULL)); - Py_XDECREF(CALL_PYTHON_AND_CHECK(PyObject_CallMethodObjArgs(running_loop, remove_reader_str, self->reader_fd, NULL))); - Py_RETURN_NONE; -} +#define CHECK_ASYNCIO_WATCHERS ({ CALL_PYTHON_EXPECT_NONE(SdBus_asyncio_update_fd_watchers(self)); }) -static PyObject* SdBus_drive(SdBusObject* self, PyObject* Py_UNUSED(args)) { +static PyObject* SdBus_process(SdBusObject* self, PyObject* Py_UNUSED(args)) { int return_value = 1; while (return_value > 0) { return_value = sd_bus_process(self->sd_bus_ref, NULL); if (return_value < 0) { - CALL_PYTHON_AND_CHECK(unregister_reader(self)); if (-ECONNRESET == return_value) { // Connection gracefully terminated - Py_RETURN_NONE; + break; } else { // Error occurred processing sdbus CALL_SD_BUS_AND_CHECK(return_value); @@ -280,6 +259,7 @@ static PyObject* SdBus_drive(SdBusObject* self, PyObject* Py_UNUSED(args)) { return NULL; } } + CHECK_ASYNCIO_WATCHERS; Py_RETURN_NONE; } @@ -291,7 +271,7 @@ int SdBus_async_callback(sd_bus_message* m, PyObject* py_future = userdata; PyObject* is_cancelled CLEANUP_PY_OBJECT = PyObject_CallMethod(py_future, "cancelled", ""); if (Py_True == is_cancelled) { - // A bit unpythonic but SdBus_drive does not error out + // A bit unpythonic but SdBus_process does not error out return 0; } @@ -340,7 +320,7 @@ static PyObject* SdBus_call_async(SdBusObject* self, PyObject* args) { if (PyObject_SetAttrString(new_future, "_sd_bus_py_slot", (PyObject*)new_slot_object) < 0) { return NULL; } - CHECK_SD_BUS_READER; + CHECK_ASYNCIO_WATCHERS; return new_future; } @@ -454,7 +434,7 @@ static PyObject* SdBus_match_signal_async(SdBusObject* self, PyObject* args) { interface_name_char_ptr, member_name_char_ptr, _SdBus_signal_callback, _SdBus_match_signal_instant_callback, new_future)); - CHECK_SD_BUS_READER; + CHECK_ASYNCIO_WATCHERS; Py_INCREF(new_future); return new_future; } @@ -465,7 +445,7 @@ int SdBus_request_name_callback(sd_bus_message* m, PyObject* py_future = userdata; PyObject* is_cancelled CLEANUP_PY_OBJECT = PyObject_CallMethod(py_future, "cancelled", ""); if (Py_True == is_cancelled) { - // A bit unpythonic but SdBus_drive does not error out + // A bit unpythonic but SdBus_process does not error out return 0; } @@ -531,7 +511,7 @@ static PyObject* SdBus_request_name_async(SdBusObject* self, PyObject* args) { sd_bus_request_name_async(self->sd_bus_ref, &new_slot_object->slot_ref, service_name_char_ptr, flags, SdBus_request_name_callback, new_future)); CALL_PYTHON_INT_CHECK(PyObject_SetAttrString(new_future, "_sd_bus_py_slot", (PyObject*)new_slot_object)); - CHECK_SD_BUS_READER; + CHECK_ASYNCIO_WATCHERS; return new_future; } @@ -635,10 +615,49 @@ static PyObject* SdBus_start(SdBusObject* self, PyObject* Py_UNUSED(args)) { Py_RETURN_NONE; } +static inline int sd_bus_get_events_zero_on_closed(SdBusObject* self) { + int events = sd_bus_get_events(self->sd_bus_ref); + if (-ENOTCONN == events) { + return 0; + } + return events; +}; + +static PyObject* SdBus_asyncio_update_fd_watchers(SdBusObject* self) { + int events_to_watch = CALL_SD_BUS_AND_CHECK(sd_bus_get_events_zero_on_closed(self)); + if (events_to_watch == self->asyncio_watchers_last_state) { + // Do not update the watchers because state is the same + Py_RETURN_NONE; + } else { + self->asyncio_watchers_last_state = events_to_watch; + } + + PyObject* running_loop CLEANUP_PY_OBJECT = CALL_PYTHON_AND_CHECK(PyObject_CallFunctionObjArgs(asyncio_get_running_loop, NULL)); + PyObject* drive_method CLEANUP_PY_OBJECT = CALL_PYTHON_AND_CHECK(PyObject_GetAttrString((PyObject*)self, "process")); + + if (NULL == self->bus_fd) { + self->bus_fd = CALL_PYTHON_AND_CHECK(SdBus_get_fd(self, NULL)); + } + + if (events_to_watch & POLLIN) { + Py_XDECREF(CALL_PYTHON_AND_CHECK(PyObject_CallMethodObjArgs(running_loop, add_reader_str, self->bus_fd, drive_method, NULL))); + } else { + Py_XDECREF(CALL_PYTHON_AND_CHECK(PyObject_CallMethodObjArgs(running_loop, remove_reader_str, self->bus_fd, NULL))); + } + + if (events_to_watch & POLLOUT) { + Py_XDECREF(CALL_PYTHON_AND_CHECK(PyObject_CallMethodObjArgs(running_loop, add_writer_str, self->bus_fd, drive_method, NULL))); + } else { + Py_XDECREF(CALL_PYTHON_AND_CHECK(PyObject_CallMethodObjArgs(running_loop, remove_writer_str, self->bus_fd, NULL))); + } + + Py_RETURN_NONE; +} + 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.")}, - {"drive", (PyCFunction)SdBus_drive, METH_NOARGS, PyDoc_STR("Drive connection.")}, + {"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.")}, {"new_property_get_message", (SD_BUS_PY_FUNC_TYPE)SdBus_new_property_get_message, SD_BUS_PY_METH, PyDoc_STR("Create new empty property get message.")}, diff --git a/test/test_sdbus_async.py b/test/test_sdbus_async.py index e8be301..c7c8a94 100644 --- a/test/test_sdbus_async.py +++ b/test/test_sdbus_async.py @@ -234,6 +234,10 @@ async def takes_struct_method( a, b, c, d = int_struct return a*b*c*d + @dbus_method_async("s", "x") + async def return_length(self, input_str: str) -> int: + return len(input_str) + class DbusErrorTest(DbusFailedError): dbus_error_name = 'org.example.Error' @@ -939,3 +943,24 @@ async def two(self) -> int: class CombinedInterface(OneInterface, TwoInterface): ... + + async def test_extremely_large_string(self) -> None: + test_object, test_object_connection = initialize_object() + + extremely_large_string = "a" * 8423681 + + remote_len = await wait_for( + test_object_connection.return_length( + extremely_large_string + ), + timeout=10, + ) + + self.assertEqual( + remote_len, + len(extremely_large_string), + ) + + # Check that calling regular methods still works. + for _ in range(5): + await test_object_connection.returns_none_method() From aaf63a340cca65bad6cdd154ea2a4038391e4f37 Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sat, 24 Feb 2024 23:01:35 +0600 Subject: [PATCH 074/188] Return async D-Bus elements directly when accessed from class Meaning it will be equivalent to how when function is accessed from object it becomes method but is still function when accessed from class. This removes the *ClassBind classes and returns the D-Bus element itself like DbusPropertyAsync or DbusMethodAsync. --- src/sdbus/autodoc.py | 35 ++---- src/sdbus/dbus_proxy_async_interface_base.py | 15 +-- src/sdbus/dbus_proxy_async_method.py | 39 ++++--- src/sdbus/dbus_proxy_async_property.py | 38 ++++--- src/sdbus/dbus_proxy_async_signal.py | 108 +++++++++---------- test/test_typing.py | 29 ++++- 6 files changed, 137 insertions(+), 127 deletions(-) diff --git a/src/sdbus/autodoc.py b/src/sdbus/autodoc.py index 11fe41c..4bfafb6 100644 --- a/src/sdbus/autodoc.py +++ b/src/sdbus/autodoc.py @@ -23,12 +23,9 @@ from sphinx.ext.autodoc import AttributeDocumenter, MethodDocumenter -from .dbus_proxy_async_method import DbusMethodAsyncClassBind -from .dbus_proxy_async_property import ( - DbusPropertyAsync, - DbusPropertyAsyncClassBind, -) -from .dbus_proxy_async_signal import DbusSignalAsync, DbusSignalAsyncClassBind +from .dbus_proxy_async_method import DbusMethodAsync +from .dbus_proxy_async_property import DbusPropertyAsync +from .dbus_proxy_async_signal import DbusSignalAsync if TYPE_CHECKING: from typing import Any, Dict @@ -44,14 +41,12 @@ class DbusMethodDocumenter(MethodDocumenter): @classmethod def can_document_member(cls, member: Any, *args: Any) -> bool: - return isinstance(member, DbusMethodAsyncClassBind) + return isinstance(member, DbusMethodAsync) def import_object(self, raiseerror: bool = False) -> bool: - self.objpath.append('dbus_method') self.objpath.append('original_method') ret = super().import_object(raiseerror) self.objpath.pop() - self.objpath.pop() return ret def add_content(self, @@ -68,20 +63,13 @@ def add_content(self, class DbusPropertyDocumenter(AttributeDocumenter): - objtype = 'DbusPropertyAsyncClassBind' + objtype = 'DbusPropertyAsync' directivetype = 'attribute' priority = 100 + AttributeDocumenter.priority @classmethod def can_document_member(cls, member: Any, *args: Any) -> bool: - return isinstance(member, DbusPropertyAsyncClassBind) - - def import_object(self, raiseerror: bool = False) -> bool: - - self.objpath.append('dbus_property') - ret = super().import_object(raiseerror) - self.objpath.pop() - return ret + return isinstance(member, DbusPropertyAsync) def add_content(self, *args: Any, **kwargs: Any, @@ -109,20 +97,13 @@ def add_content(self, class DbusSignalDocumenter(AttributeDocumenter): - objtype = 'DbusSignalAsyncClassBind' + objtype = 'DbusSignalAsync' directivetype = 'attribute' priority = 100 + AttributeDocumenter.priority @classmethod def can_document_member(cls, member: Any, *args: Any) -> bool: - return isinstance(member, DbusSignalAsyncClassBind) - - def import_object(self, raiseerror: bool = False) -> bool: - - self.objpath.append('dbus_signal') - ret = super().import_object(raiseerror) - self.objpath.pop() - return ret + return isinstance(member, DbusSignalAsync) def add_content(self, *args: Any, **kwargs: Any, diff --git a/src/sdbus/dbus_proxy_async_interface_base.py b/src/sdbus/dbus_proxy_async_interface_base.py index 1b686b3..3dc6ff7 100644 --- a/src/sdbus/dbus_proxy_async_interface_base.py +++ b/src/sdbus/dbus_proxy_async_interface_base.py @@ -36,14 +36,9 @@ DbusSomethingSync, ) from .dbus_common_funcs import get_default_bus -from .dbus_proxy_async_method import ( - DbusMethodAsync, - DbusMethodAsyncClassBind, - DbusMethodAsyncLocalBind, -) +from .dbus_proxy_async_method import DbusMethodAsync, DbusMethodAsyncLocalBind from .dbus_proxy_async_property import ( DbusPropertyAsync, - DbusPropertyAsyncClassBind, DbusPropertyAsyncLocalBind, ) from .dbus_proxy_async_signal import DbusSignalAsync, DbusSignalAsyncLocalBind @@ -162,12 +157,12 @@ def __new__(cls, name: str, super_element = getattr(base, collision_name) dbus_element_override: DbusSomethingAsync - if isinstance(super_element, DbusMethodAsyncClassBind): - dbus_element_override = copy(super_element.dbus_method) + if isinstance(super_element, DbusMethodAsync): + dbus_element_override = copy(super_element) dbus_element_override.original_method = cast( MethodType, override.original) - elif isinstance(super_element, DbusPropertyAsyncClassBind): - dbus_element_override = copy(super_element.dbus_property) + elif isinstance(super_element, DbusPropertyAsync): + dbus_element_override = copy(super_element) dbus_element_override.property_getter = cast( Callable[[DbusInterfaceBaseAsync], Any], override.original) diff --git a/src/sdbus/dbus_proxy_async_method.py b/src/sdbus/dbus_proxy_async_method.py index f7d39c1..d326674 100644 --- a/src/sdbus/dbus_proxy_async_method.py +++ b/src/sdbus/dbus_proxy_async_method.py @@ -22,7 +22,7 @@ from contextvars import ContextVar, copy_context from inspect import iscoroutinefunction from types import FunctionType -from typing import TYPE_CHECKING, cast +from typing import TYPE_CHECKING, cast, overload from weakref import ref as weak_ref from .dbus_common_elements import ( @@ -36,7 +36,7 @@ from .sd_bus_internals import DbusNoReplyFlag if TYPE_CHECKING: - from typing import Any, Callable, Optional, Sequence, Type, TypeVar + from typing import Any, Callable, Optional, Sequence, Type, TypeVar, Union from .dbus_proxy_async_interface_base import DbusInterfaceBaseAsync from .sd_bus_internals import SdBusMessage @@ -53,10 +53,28 @@ def get_current_message() -> SdBusMessage: class DbusMethodAsync(DbusMethodCommon, DbusSomethingAsync): - def __get__(self, - obj: Optional[DbusInterfaceBaseAsync], - obj_class: Optional[Type[DbusInterfaceBaseAsync]] = None, - ) -> Callable[..., Any]: + + @overload + def __get__( + self, + obj: None, + obj_class: Type[DbusInterfaceBaseAsync], + ) -> DbusMethodAsync: + ... + + @overload + def __get__( + self, + obj: DbusInterfaceBaseAsync, + obj_class: Type[DbusInterfaceBaseAsync], + ) -> Callable[..., Any]: + ... + + def __get__( + self, + obj: Optional[DbusInterfaceBaseAsync], + obj_class: Optional[Type[DbusInterfaceBaseAsync]] = None, + ) -> Union[Callable[..., Any], DbusMethodAsync]: if obj is not None: dbus_meta = obj._dbus if isinstance(dbus_meta, DbusRemoteObjectMeta): @@ -64,7 +82,7 @@ def __get__(self, else: return DbusMethodAsyncLocalBind(self, obj) else: - return DbusMethodAsyncClassBind(self) + return self class DbusMethodAsyncBaseBind(DbusBindedAsync): @@ -214,13 +232,6 @@ async def _dbus_reply_call( reply_message.send() -class DbusMethodAsyncClassBind(DbusMethodAsyncBaseBind): - def __init__(self, dbus_method: DbusMethodAsync): - self.dbus_method = dbus_method - - self.__doc__ = dbus_method.__doc__ - - def dbus_method_async( input_signature: str = "", result_signature: str = "", diff --git a/src/sdbus/dbus_proxy_async_property.py b/src/sdbus/dbus_proxy_async_property.py index b5139aa..2af1ce3 100644 --- a/src/sdbus/dbus_proxy_async_property.py +++ b/src/sdbus/dbus_proxy_async_property.py @@ -21,7 +21,7 @@ from inspect import iscoroutinefunction from types import FunctionType -from typing import TYPE_CHECKING, Awaitable, Generic, TypeVar, cast +from typing import TYPE_CHECKING, Awaitable, Generic, TypeVar, cast, overload from weakref import ref as weak_ref from .dbus_common_elements import ( @@ -33,7 +33,7 @@ ) if TYPE_CHECKING: - from typing import Any, Callable, Generator, Optional, Type + from typing import Any, Callable, Generator, Optional, Type, Union from .dbus_proxy_async_interface_base import DbusInterfaceBaseAsync from .sd_bus_internals import SdBusMessage @@ -71,10 +71,27 @@ def __init__( self.__doc__ = property_getter.__doc__ - def __get__(self, - obj: Optional[DbusInterfaceBaseAsync], - obj_class: Optional[Type[DbusInterfaceBaseAsync]] = None, - ) -> DbusPropertyAsyncBaseBind[T]: + @overload + def __get__( + self, + obj: None, + obj_class: Type[DbusInterfaceBaseAsync], + ) -> DbusPropertyAsync[T]: + ... + + @overload + def __get__( + self, + obj: DbusInterfaceBaseAsync, + obj_class: Type[DbusInterfaceBaseAsync], + ) -> DbusPropertyAsyncBaseBind[T]: + ... + + def __get__( + self, + obj: Optional[DbusInterfaceBaseAsync], + obj_class: Optional[Type[DbusInterfaceBaseAsync]] = None, + ) -> Union[DbusPropertyAsyncBaseBind[T], DbusPropertyAsync[T]]: if obj is not None: dbus_meta = obj._dbus if isinstance(dbus_meta, DbusRemoteObjectMeta): @@ -82,7 +99,7 @@ def __get__(self, else: return DbusPropertyAsyncLocalBind(self, obj) else: - return DbusPropertyAsyncClassBind(self) + return self def setter(self, new_set_function: Callable[ @@ -254,13 +271,6 @@ def _dbus_reply_set(self, message: SdBusMessage) -> None: ) -class DbusPropertyAsyncClassBind(DbusPropertyAsyncBaseBind[T]): - def __init__(self, dbus_property: DbusPropertyAsync[T]): - self.dbus_property = dbus_property - - self.__doc__ = dbus_property.__doc__ - - def dbus_property_async( property_signature: str = "", flags: int = 0, diff --git a/src/sdbus/dbus_proxy_async_signal.py b/src/sdbus/dbus_proxy_async_signal.py index 39a2329..0aca3e7 100644 --- a/src/sdbus/dbus_proxy_async_signal.py +++ b/src/sdbus/dbus_proxy_async_signal.py @@ -29,6 +29,7 @@ Generic, TypeVar, cast, + overload, ) from weakref import WeakSet @@ -42,7 +43,7 @@ from .dbus_common_funcs import get_default_bus if TYPE_CHECKING: - from typing import Any, Callable, Optional, Sequence, Tuple, Type + from typing import Any, Callable, Optional, Sequence, Tuple, Type, Union from .dbus_proxy_async_interface_base import DbusInterfaceBaseAsync from .sd_bus_internals import SdBus, SdBusMessage, SdBusSlot @@ -71,11 +72,27 @@ def __init__( self.local_callbacks: WeakSet[Callable[[T], Any]] = WeakSet() + @overload + def __get__( + self, + obj: None, + obj_class: Type[DbusInterfaceBaseAsync], + ) -> DbusSignalAsync[T]: + ... + + @overload + def __get__( + self, + obj: DbusInterfaceBaseAsync, + obj_class: Type[DbusInterfaceBaseAsync], + ) -> DbusSignalAsyncBaseBind[T]: + ... + def __get__( self, obj: Optional[DbusInterfaceBaseAsync], obj_class: Optional[Type[DbusInterfaceBaseAsync]] = None, - ) -> DbusSignalAsyncBaseBind[T]: + ) -> Union[DbusSignalAsyncBaseBind[T], DbusSignalAsync[T]]: if obj is not None: dbus_meta = obj._dbus if isinstance(dbus_meta, DbusRemoteObjectMeta): @@ -83,7 +100,35 @@ def __get__( else: return DbusSignalAsyncLocalBind(self, dbus_meta) else: - return DbusSignalAsyncClassBind(self) + return self + + async def catch_anywhere( + self, + service_name: str, + bus: Optional[SdBus] = None, + ) -> AsyncIterable[Tuple[str, T]]: + if bus is None: + bus = get_default_bus() + + message_queue: Queue[SdBusMessage] = Queue() + + match_slot = await bus.match_signal_async( + service_name, + None, + self.interface_name, + self.signal_name, + message_queue.put_nowait, + ) + + with closing(match_slot): + while True: + next_signal_message = await message_queue.get() + signal_path = next_signal_message.path + assert signal_path is not None + yield ( + signal_path, + cast(T, next_signal_message.get_contents()) + ) class DbusSignalAsyncBaseBind(DbusBindedAsync, AsyncIterable[T], Generic[T]): @@ -248,63 +293,6 @@ def emit(self, args: T) -> None: callback(args) -class DbusSignalAsyncClassBind(DbusSignalAsyncBaseBind[T]): - def __init__( - self, - dbus_signal: DbusSignalAsync[T], - ): - self.dbus_signal = dbus_signal - - self.__doc__ = dbus_signal.__doc__ - - async def catch(self) -> AsyncIterator[T]: - raise NotImplementedError( - "Cannot catch D-Bus signal from class." - ) - yield - - __aiter__ = catch - - async def catch_anywhere( - self, - service_name: Optional[str] = None, - bus: Optional[SdBus] = None, - ) -> AsyncIterable[Tuple[str, T]]: - if service_name is None: - raise ValueError( - 'Called catch_anywhere from class ' - 'but service name was not provided.' - ) - - if bus is None: - bus = get_default_bus() - - message_queue: Queue[SdBusMessage] = Queue() - - match_slot = await bus.match_signal_async( - service_name, - None, - self.dbus_signal.interface_name, - self.dbus_signal.signal_name, - message_queue.put_nowait, - ) - - with closing(match_slot): - while True: - next_signal_message = await message_queue.get() - signal_path = next_signal_message.path - assert signal_path is not None - yield ( - signal_path, - cast(T, next_signal_message.get_contents()) - ) - - def emit(self, args: T) -> None: - raise NotImplementedError( - "Cannot emit D-Bus signal from class." - ) - - def dbus_signal_async( signal_signature: str = '', signal_args_names: Sequence[str] = (), diff --git a/test/test_typing.py b/test/test_typing.py index 0197eb4..a84b872 100644 --- a/test/test_typing.py +++ b/test/test_typing.py @@ -138,10 +138,35 @@ async def check_async_interface_signal_typing( for x2 in ls2: x2.capitalize() - async for p, ls3 in test_interface.str_list_signal.catch_anywhere(): - p.capitalize() + async for p1, ls3 in test_interface.str_list_signal.catch_anywhere(): + p1.capitalize() ls3.append("test") for x3 in ls3: x3.capitalize() + async for p2, ls4 in ( + TestTypingAsync.str_list_signal + .catch_anywhere("org.example") + ): + p2.capitalize() + ls4.append("test") + for x4 in ls4: + x4.capitalize() + test_interface.str_list_signal.emit(["test", "foobar"]) + + +async def check_async_element_class_access_typing() -> None: + + test_list: List[str] = [] + + # TODO: Fix dbus async method typing + # test_list.append( + # TestTypingAsync.get_str_list_method.method_name + # ) + test_list.append( + TestTypingAsync.str_list_property.property_name + ) + test_list.append( + TestTypingAsync.str_list_signal.signal_name + ) From 38485ff58666afe5da393ced2e8804b0b951e0e8 Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sun, 25 Feb 2024 16:39:05 +0600 Subject: [PATCH 075/188] CI: Update checkout action to latest version --- .github/workflows/ubuntu_pypi_test.yml | 2 +- .github/workflows/ubuntu_test.yml | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ubuntu_pypi_test.yml b/.github/workflows/ubuntu_pypi_test.yml index 811ed91..9a1bc6c 100644 --- a/.github/workflows/ubuntu_pypi_test.yml +++ b/.github/workflows/ubuntu_pypi_test.yml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-20.04 steps: - name: Checkout - uses: actions/checkout@755da8c3cf115ac066823e79a1e1788f8940201b + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 - name: Install dependencies run: | sudo apt update diff --git a/.github/workflows/ubuntu_test.yml b/.github/workflows/ubuntu_test.yml index 28ab86b..261fcc7 100644 --- a/.github/workflows/ubuntu_test.yml +++ b/.github/workflows/ubuntu_test.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-20.04 steps: - name: Checkout - uses: actions/checkout@755da8c3cf115ac066823e79a1e1788f8940201b + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 - name: Install dependencies run: | sudo apt update @@ -29,7 +29,7 @@ jobs: runs-on: ubuntu-20.04 steps: - name: Checkout - uses: actions/checkout@755da8c3cf115ac066823e79a1e1788f8940201b + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 - name: Install dependencies run: | sudo apt update @@ -48,7 +48,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@755da8c3cf115ac066823e79a1e1788f8940201b + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 - name: Install dependencies run: | sudo apt update @@ -64,7 +64,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@755da8c3cf115ac066823e79a1e1788f8940201b + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 - name: Build Alpine container run: | podman build --tag alpine-ci -f ./test/containers/Containerfile-alpine . From 72e9d52f91a27e499a9b0c2890d307c13496939e Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sun, 25 Feb 2024 16:10:04 +0600 Subject: [PATCH 076/188] Use ParamSpec for DbusMethodAsync instead of masking it Originally the DbusMethodAsync was masked under the original function meaning type checker treated it as it was the original function and not a DbusMethodAsync. However, it is planned to add new methods to the DbusMethodAsync so masking it is no longer an option. ParamSpec is only available since Python 3.10 so use the `typing_extensions` import hidden under TYPE_CHECKING if statement. --- src/sdbus/dbus_proxy_async_method.py | 87 +++++++++++++++++++++------- test/test_sdbus_async_bad_class.py | 2 +- test/test_typing.py | 7 +-- 3 files changed, 69 insertions(+), 27 deletions(-) diff --git a/src/sdbus/dbus_proxy_async_method.py b/src/sdbus/dbus_proxy_async_method.py index d326674..97e671d 100644 --- a/src/sdbus/dbus_proxy_async_method.py +++ b/src/sdbus/dbus_proxy_async_method.py @@ -22,7 +22,7 @@ from contextvars import ContextVar, copy_context from inspect import iscoroutinefunction from types import FunctionType -from typing import TYPE_CHECKING, cast, overload +from typing import TYPE_CHECKING, Generic, TypeVar, cast, overload from weakref import ref as weak_ref from .dbus_common_elements import ( @@ -36,14 +36,27 @@ from .sd_bus_internals import DbusNoReplyFlag if TYPE_CHECKING: - from typing import Any, Callable, Optional, Sequence, Type, TypeVar, Union + from typing import ( + Any, + Callable, + Coroutine, + Optional, + Sequence, + Type, + Union, + ) + + from typing_extensions import Concatenate, ParamSpec from .dbus_proxy_async_interface_base import DbusInterfaceBaseAsync from .sd_bus_internals import SdBusMessage - T = TypeVar('T') + TDBI = TypeVar("TDBI", bound=DbusInterfaceBaseAsync) + P = ParamSpec("P") else: - T = None + P = TypeVar("P") + +TR = TypeVar("TR") CURRENT_MESSAGE: ContextVar[SdBusMessage] = ContextVar('CURRENT_MESSAGE') @@ -52,14 +65,18 @@ def get_current_message() -> SdBusMessage: return CURRENT_MESSAGE.get() -class DbusMethodAsync(DbusMethodCommon, DbusSomethingAsync): +class DbusMethodAsync( + DbusMethodCommon, + DbusSomethingAsync, + Generic[P, TR], +): @overload def __get__( self, obj: None, obj_class: Type[DbusInterfaceBaseAsync], - ) -> DbusMethodAsync: + ) -> DbusMethodAsync[P, TR]: ... @overload @@ -67,14 +84,14 @@ def __get__( self, obj: DbusInterfaceBaseAsync, obj_class: Type[DbusInterfaceBaseAsync], - ) -> Callable[..., Any]: + ) -> DbusMethodAsyncBaseBind[P, TR]: ... def __get__( self, obj: Optional[DbusInterfaceBaseAsync], obj_class: Optional[Type[DbusInterfaceBaseAsync]] = None, - ) -> Union[Callable[..., Any], DbusMethodAsync]: + ) -> Union[DbusMethodAsyncBaseBind[P, TR], DbusMethodAsync[P, TR]]: if obj is not None: dbus_meta = obj._dbus if isinstance(dbus_meta, DbusRemoteObjectMeta): @@ -85,16 +102,22 @@ def __get__( return self -class DbusMethodAsyncBaseBind(DbusBindedAsync): +class DbusMethodAsyncBaseBind( + DbusBindedAsync, + Generic[P, TR], +): - def __call__(self, *args: Any, **kwargs: Any) -> Any: + def __call__( + *args: P.args, + **kwargs: P.kwargs, + ) -> Coroutine[Any, Any, TR]: raise NotImplementedError -class DbusMethodAsyncProxyBind(DbusMethodAsyncBaseBind): +class DbusMethodAsyncProxyBind(DbusMethodAsyncBaseBind[P, TR]): def __init__( self, - dbus_method: DbusMethodAsync, + dbus_method: DbusMethodAsync[P, TR], proxy_meta: DbusRemoteObjectMeta, ): self.dbus_method = dbus_method @@ -111,7 +134,7 @@ async def _dbus_async_call(self, call_message: SdBusMessage) -> Any: async def _no_reply() -> None: return None - def __call__(self, *args: Any, **kwargs: Any) -> Any: + def __call__(self, *args: P.args, **kwargs: P.kwargs) -> Any: bus = self.proxy_meta.attached_bus dbus_method = self.dbus_method @@ -145,10 +168,10 @@ def __call__(self, *args: Any, **kwargs: Any) -> Any: return self._dbus_async_call(new_call_message) -class DbusMethodAsyncLocalBind(DbusMethodAsyncBaseBind): +class DbusMethodAsyncLocalBind(DbusMethodAsyncBaseBind[P, TR]): def __init__( self, - dbus_method: DbusMethodAsync, + dbus_method: DbusMethodAsync[P, TR], local_object: DbusInterfaceBaseAsync, ): self.dbus_method = dbus_method @@ -156,7 +179,7 @@ def __init__( self.__doc__ = dbus_method.__doc__ - def __call__(self, *args: Any, **kwargs: Any) -> Any: + def __call__(self, *args: P.args, **kwargs: P.kwargs) -> Any: local_object = self.local_object_ref() if local_object is None: raise RuntimeError("Local object no longer exists!") @@ -239,14 +262,24 @@ def dbus_method_async( result_args_names: Optional[Sequence[str]] = None, input_args_names: Optional[Sequence[str]] = None, method_name: Optional[str] = None, -) -> Callable[[T], T]: +) -> Callable[ + [Callable[ + Concatenate[TDBI, P], + Coroutine[Any, Any, TR]]], + DbusMethodAsync[P, TR], +]: assert not isinstance(input_signature, FunctionType), ( "Passed function to decorator directly. " "Did you forget () round brackets?" ) - def dbus_method_decorator(original_method: T) -> T: + def dbus_method_decorator( + original_method: Callable[ + Concatenate[TDBI, P], + Coroutine[Any, Any, TR] + ], + ) -> DbusMethodAsync[P, TR]: assert isinstance(original_method, FunctionType) assert iscoroutinefunction(original_method), ( "Expected coroutine function. ", @@ -262,15 +295,25 @@ def dbus_method_decorator(original_method: T) -> T: flags=flags, ) - return cast(T, new_wrapper) + return new_wrapper return dbus_method_decorator -def dbus_method_async_override() -> Callable[[T], T]: +def dbus_method_async_override( +) -> Callable[ + [Callable[ + Concatenate[TDBI, P], + Coroutine[Any, Any, TR]]], + DbusMethodAsync[P, TR], +]: def new_decorator( - new_function: T) -> T: - return cast(T, DbusOverload(new_function)) + new_function: Callable[ + Concatenate[TDBI, P], + Coroutine[Any, Any, TR] + ], + ) -> DbusMethodAsync[P, TR]: + return cast(DbusMethodAsync[P, TR], DbusOverload(new_function)) return new_decorator diff --git a/test/test_sdbus_async_bad_class.py b/test/test_sdbus_async_bad_class.py index a8b345a..9aa8b95 100644 --- a/test/test_sdbus_async_bad_class.py +++ b/test/test_sdbus_async_bad_class.py @@ -163,7 +163,7 @@ def test_bad_subclass(self) -> None: with self.assertRaises(TypeError): class TestInheritence(TestInterface): - async def test_int(self) -> int: + async def test_int(self) -> int: # type: ignore[override] return 2 with self.assertRaises(TypeError): diff --git a/test/test_typing.py b/test/test_typing.py index a84b872..7feb9ef 100644 --- a/test/test_typing.py +++ b/test/test_typing.py @@ -160,10 +160,9 @@ async def check_async_element_class_access_typing() -> None: test_list: List[str] = [] - # TODO: Fix dbus async method typing - # test_list.append( - # TestTypingAsync.get_str_list_method.method_name - # ) + test_list.append( + TestTypingAsync.get_str_list_method.method_name + ) test_list.append( TestTypingAsync.str_list_property.property_name ) From b6a19742033ef4c463a8260b360e3ad072b5c544 Mon Sep 17 00:00:00 2001 From: igo95862 Date: Mon, 4 Mar 2024 00:57:52 +0500 Subject: [PATCH 077/188] Document IsolatedDbusTestCase.assertDbusSignalEmits Also the recorder object now only has the `.output` property. The tests can call the assert methods against it to compare the emitted data. --- docs/unittest.rst | 81 +++++++++++++++++++++++++++------------- src/sdbus/unittest.py | 23 ++++-------- test/test_sdbus_async.py | 8 ++-- 3 files changed, 66 insertions(+), 46 deletions(-) diff --git a/docs/unittest.rst b/docs/unittest.rst index d7f5364..100b653 100644 --- a/docs/unittest.rst +++ b/docs/unittest.rst @@ -16,6 +16,43 @@ Python-sdbus provides several utilities to enable unit testing. Requires ``dbus-daemon`` executable be installed. + Example:: + + from sdbus import DbusInterfaceCommonAsync, dbus_method_async + from sdbus.unittest import IsolatedDbusTestCase + + class TestInterface(DbusInterfaceCommonAsync, + interface_name='org.test.test', + ): + + @dbus_method_async("s", "s") + async def upper(self, string: str) -> str: + """Uppercase the input""" + return string.upper() + + def initialize_object() -> Tuple[TestInterface, TestInterface]: + test_object = TestInterface() + test_object.export_to_dbus('/') + + test_object_connection = TestInterface.new_proxy( + "org.example.test", '/') + + return test_object, test_object_connection + + + class TestProxy(IsolatedDbusTestCase): + async def asyncSetUp(self) -> None: + await super().asyncSetUp() + await self.bus.request_name_async("org.example.test", 0) + + async def test_method_kwargs(self) -> None: + test_object, test_object_connection = initialize_object() + + self.assertEqual( + 'TEST', + await test_object_connection.upper('test'), + ) + .. py:attribute:: bus :type: SdBus @@ -23,40 +60,32 @@ Python-sdbus provides several utilities to enable unit testing. It is also set as a default bus. + .. py:method:: assertDbusSignalEmits(signal, timeout=1) + + Assert that a given signal was emitted at least once within the + given timeout. + + :param signal: D-Bus signal object. Can be a signal from either local or proxy object. + :param Union[int, float] timeout: Maximum wait time until first captured signal. -Usage example: :: + Should be used as an async context manager. The context manager exits as soon + as first signal is captured. - from sdbus import DbusInterfaceCommonAsync, dbus_method_async - from sdbus.unittest import IsolatedDbusTestCase + The object returned by context manager has following attributes: - class TestInterface(DbusInterfaceCommonAsync, - interface_name='org.test.test', - ): + .. py:attribute:: output + :type: List[Any] - @dbus_method_async("s", "s") - async def upper(self, string: str) -> str: - """Uppercase the input""" - return string.upper() + List of captured data. - def initialize_object() -> Tuple[TestInterface, TestInterface]: - test_object = TestInterface() - test_object.export_to_dbus('/') + Example:: - test_object_connection = TestInterface.new_proxy( - "org.example.test", '/') + async with self.assertDbusSignalEmits(test_object.test_signal) as signal_record: + test_object.test_signal.emit("test") - return test_object, test_object_connection + self.assertEqual(["test"], signal_record.output) + *New in version 0.12.0.* - class TestProxy(IsolatedDbusTestCase): - async def asyncSetUp(self) -> None: - await super().asyncSetUp() - await self.bus.request_name_async("org.example.test", 0) - async def test_method_kwargs(self) -> None: - test_object, test_object_connection = initialize_object() - self.assertEqual( - 'TEST', - await test_object_connection.upper('test'), - ) diff --git a/src/sdbus/unittest.py b/src/sdbus/unittest.py index 5fc5245..63a0d47 100644 --- a/src/sdbus/unittest.py +++ b/src/sdbus/unittest.py @@ -75,10 +75,8 @@ class DbusSignalRecorderBase: def __init__( self, - testcase: IsolatedDbusTestCase, timeout: Union[int, float], ): - self._testcase = testcase self._timeout = timeout self._captured_data: List[Any] = [] self._ready_event = Event() @@ -114,25 +112,19 @@ def _callback(self, data: Any) -> None: self._captured_data.append(data) self._ready_event.set() - def assert_emitted_once_with(self, data: Any) -> None: - captured_signals_num = len(self._captured_data) - if captured_signals_num != 1: - raise AssertionError( - f"Expected one captured signal got {captured_signals_num}" - ) - - self._testcase.assertEqual(self._captured_data[0], data) + @property + def output(self) -> List[Any]: + return self._captured_data.copy() class DbusSignalRecorderRemote(DbusSignalRecorderBase): def __init__( self, - testcase: IsolatedDbusTestCase, timeout: Union[int, float], bus: SdBus, remote_signal: DbusSignalAsyncProxyBind[Any], ): - super().__init__(testcase, timeout) + super().__init__(timeout) self._bus = bus self._match_slot: Optional[SdBusSlot] = None self._remote_signal = remote_signal @@ -161,11 +153,10 @@ async def __aexit__( class DbusSignalRecorderLocal(DbusSignalRecorderBase): def __init__( self, - testcase: IsolatedDbusTestCase, timeout: Union[int, float], local_signal: DbusSignalAsyncLocalBind[Any], ): - super().__init__(testcase, timeout) + super().__init__(timeout) self._local_signal_ref: weak_ref[DbusSignalAsync[Any]] = ( weak_ref(local_signal.dbus_signal) ) @@ -234,8 +225,8 @@ def assertDbusSignalEmits( ) -> AsyncContextManager[DbusSignalRecorderBase]: if isinstance(signal, DbusSignalAsyncLocalBind): - return DbusSignalRecorderLocal(self, timeout, signal) + return DbusSignalRecorderLocal(timeout, signal) elif isinstance(signal, DbusSignalAsyncProxyBind): - return DbusSignalRecorderRemote(self, timeout, self.bus, signal) + return DbusSignalRecorderRemote(timeout, self.bus, signal) else: raise TypeError("Unknown or unsupported signal class.") diff --git a/test/test_sdbus_async.py b/test/test_sdbus_async.py index c7c8a94..523ea14 100644 --- a/test/test_sdbus_async.py +++ b/test/test_sdbus_async.py @@ -484,8 +484,8 @@ async def test_signal(self) -> None: ) as remote_signals_record: test_object.test_signal.emit(test_tuple) - local_signals_record.assert_emitted_once_with(test_tuple) - remote_signals_record.assert_emitted_once_with(test_tuple) + self.assertEqual([test_tuple], local_signals_record.output) + self.assertEqual([test_tuple], remote_signals_record.output) async def test_signal_catch_anywhere(self) -> None: test_object, test_object_connection = initialize_object() @@ -779,8 +779,8 @@ async def test_empty_signal(self) -> None: ) as remote_signals_record: test_object.empty_signal.emit(None) - local_signals_record.assert_emitted_once_with(None) - remote_signals_record.assert_emitted_once_with(None) + self.assertEqual([None], local_signals_record.output) + self.assertEqual([None], remote_signals_record.output) async def test_properties_changed(self) -> None: test_object, test_object_connection = initialize_object() From b4af45b351d424b6b92847c851ab1f08d2b7d407 Mon Sep 17 00:00:00 2001 From: igo95862 Date: Mon, 4 Mar 2024 01:32:15 +0500 Subject: [PATCH 078/188] Document how to declare and emit empty signals Add a new section called `Asyncio advanced topics` which will have explanations of some advanced features that python-sdbus has. --- docs/asyncio_deep.rst | 42 ++++++++++++++++++++++++++++++++++++++++++ docs/index.rst | 1 + 2 files changed, 43 insertions(+) create mode 100644 docs/asyncio_deep.rst diff --git a/docs/asyncio_deep.rst b/docs/asyncio_deep.rst new file mode 100644 index 0000000..a7ee656 --- /dev/null +++ b/docs/asyncio_deep.rst @@ -0,0 +1,42 @@ +Asyncio advanced topics ++++++++++++++++++++++++++ + +.. py:currentmodule:: sdbus + +Signals without data +^^^^^^^^^^^^^^^^^^^^ + +D-Bus allows signals to not carry any data. Such signals have the +type signature of ``""``. (empty string) + +To emit such signals the :py:meth:`emit ` must +be explicitly called with ``None``. + +Example of an empty signal:: + + from asyncio import new_event_loop + from sdbus import DbusInterfaceCommonAsync, dbus_signal_async + + + class ExampleInterface( + DbusInterfaceCommonAsync, + interface_name="org.example.signal" + ): + + @dbus_signal_async("") + def name_invalidated(self) -> None: + raise NotImplementedError + + + test_object = ExampleInterface() + + + async def emit_empty_signal() -> None: + test_object.export_to_dbus("/") + + test_object.name_invalidated.emit(None) + + + loop = new_event_loop() + loop.run_until_complete(emit_empty_signal()) + diff --git a/docs/index.rst b/docs/index.rst index 1baa6b7..0735323 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -37,6 +37,7 @@ If you are unfamiliar with D-Bus you might want to read following pages: sync_api asyncio_quick asyncio_api + asyncio_deep exceptions utils examples From d8801e94f5425ab04c1c17615563cb1bb1c98539 Mon Sep 17 00:00:00 2001 From: igo95862 Date: Mon, 4 Mar 2024 01:58:22 +0500 Subject: [PATCH 079/188] docs: Add async property and signals classes instead of documenting everything in decorators Documenting everything in the decorator resulted the methods being documented as the module level functions. I.E. `sdbus.emit` instead of `DbusSignalAsync.emit`. --- docs/asyncio_api.rst | 142 ++++++++++++++++++++++------------------- docs/asyncio_deep.rst | 2 +- docs/asyncio_quick.rst | 12 ++-- 3 files changed, 82 insertions(+), 74 deletions(-) diff --git a/docs/asyncio_api.rst b/docs/asyncio_api.rst index 48ea831..1a0a2ee 100644 --- a/docs/asyncio_api.rst +++ b/docs/asyncio_api.rst @@ -343,39 +343,6 @@ Decorators :param str property_name: Force specific property name instead of constructing it based on Python function name. - Properties have following methods: - - .. py:decoratormethod:: setter(set_function) - - Defines the setter function. - This makes the property read/write instead of read-only. - - See example on how to use. - - .. py:decoratormethod:: setter_private(set_function) - - Defines the private setter function. - The setter can be called locally but property - will be read-only from D-Bus. - - Calling the setter locally will emit - :py:attr:`properties_changed ` - signal to D-Bus. - - .. py:method:: get_async() - :async: - - Get the property value. - - The property can also be directly ``await`` ed - instead of calling this method. - - .. py:method:: set_async(new_value) - :async: - - Set property value. - - Example: :: from sdbus import DbusInterfaceCommonAsync, dbus_property_async @@ -405,6 +372,43 @@ Decorators def read_write_str_setter(self, new_str: str) -> None: self.s = new_str + .. py:class:: DbusPropertyAsync + + Properties have following methods: + + .. py:decoratormethod:: setter(set_function) + + Defines the setter function. + This makes the property read/write instead of read-only. + + See example on how to use. + + .. py:decoratormethod:: setter_private(set_function) + + Defines the private setter function. + The setter can be called locally but property + will be read-only from D-Bus. + + Calling the setter locally will emit + :py:attr:`properties_changed ` + signal to D-Bus. + + .. py:method:: get_async() + :async: + + Get the property value. + + The property can also be directly ``await`` ed + instead of calling this method. + + .. py:method:: set_async(new_value) + :async: + + Set property value. + + + + .. py:decorator:: dbus_signal_async([signal_signature, [signal_args_names, [flags, [signal_name]]]]) Defines a D-Bus signal. @@ -435,58 +439,62 @@ Decorators :param str signal_name: Forces specific signal name instead of being based on Python function name. - Signals have following methods: + Example:: - .. py:method:: catch() + from sdbus import DbusInterfaceCommonAsync, dbus_signal_async - Catch D-Bus signals using the async generator for loop: - ``async for x in something.some_signal.catch():`` - This is main way to await for new events. + class ExampleInterface(DbusInterfaceCommonAsync, + interface_name='org.example.signal' + ): - Both remote and local objects operate the same way. + @dbus_signal_async('s') + def name_changed(self) -> str: + raise NotImplementedError - Signal objects can also be async iterated directly: - ``async for x in something.some_signal`` + .. py:class:: DbusSignalAsync - .. py:method:: catch_anywhere(service_name, bus) + Signals have following methods: - Catch signal independent of path. - Yields tuple of path of the object that emitted signal and signal data. + .. py:method:: catch() - ``async for path, data in something.some_signal.catch_anywhere():`` + Catch D-Bus signals using the async generator for loop: + ``async for x in something.some_signal.catch():`` - This method can be called from both an proxy object and class. - However, it cannot be called on local objects and will raise - ``NotImplementedError``. + This is main way to await for new events. - :param str service_name: - Service name of which signals belong to. - Required if called from class. When called from proxy object - the service name of the proxy will be used. + Both remote and local objects operate the same way. - :param str bus: - Optional D-Bus connection object. - If not passed when called from proxy the bus connected - to proxy will be used or when called from class default - bus will be used. + Signal objects can also be async iterated directly: + ``async for x in something.some_signal`` - .. py:method:: emit(args) + .. py:method:: catch_anywhere(service_name, bus) - Emit a new signal with *args* data. + Catch signal independent of path. + Yields tuple of path of the object that emitted signal and signal data. - Example: :: + ``async for path, data in something.some_signal.catch_anywhere():`` - from sdbus import DbusInterfaceCommonAsync, dbus_signal_async + This method can be called from both an proxy object and class. + However, it cannot be called on local objects and will raise + ``NotImplementedError``. + :param str service_name: + Service name of which signals belong to. + Required if called from class. When called from proxy object + the service name of the proxy will be used. + + :param str bus: + Optional D-Bus connection object. + If not passed when called from proxy the bus connected + to proxy will be used or when called from class default + bus will be used. + + .. py:method:: emit(args) + + Emit a new signal with *args* data. - class ExampleInterface(DbusInterfaceCommonAsync, - interface_name='org.example.signal' - ): - @dbus_signal_async('s') - def name_changed(self) -> str: - raise NotImplementedError .. py:decorator:: dbus_method_async_override() diff --git a/docs/asyncio_deep.rst b/docs/asyncio_deep.rst index a7ee656..5e433d8 100644 --- a/docs/asyncio_deep.rst +++ b/docs/asyncio_deep.rst @@ -9,7 +9,7 @@ Signals without data D-Bus allows signals to not carry any data. Such signals have the type signature of ``""``. (empty string) -To emit such signals the :py:meth:`emit ` must +To emit such signals the :py:meth:`emit ` must be explicitly called with ``None``. Example of an empty signal:: diff --git a/docs/asyncio_quick.rst b/docs/asyncio_quick.rst index b39331a..363d09e 100644 --- a/docs/asyncio_quick.rst +++ b/docs/asyncio_quick.rst @@ -265,18 +265,18 @@ To catch a signal use ``async for`` loop: :: make sure to bind it to a variable and keep it referenced otherwise garbage collector will destroy your task. -A signal can be emitted with :py:meth:`emit` method. +A signal can be emitted with :py:meth:`emit ` method. -Example: :: +Example:: example_object.name_changed.emit('test') Signals can also be caught from multiple D-Bus objects using -:py:meth:`catch_anywhere` method. The async iterator will yield -the path of the object that emitted the signal and the signal data. +:py:meth:`catch_anywhere ` method. The async +iterator will yield the path of the object that emitted the signal and the signal data. -:py:meth:`catch_anywhere` can be called from class but in such case -the service name must be provided. +:py:meth:`catch_anywhere ` can be called from +class but in such case the service name must be provided. Example:: From 24c357f1c007111c4ea2282d12d29f9015c830b9 Mon Sep 17 00:00:00 2001 From: igo95862 Date: Mon, 4 Mar 2024 02:04:00 +0500 Subject: [PATCH 080/188] docs: Fix typos --- docs/asyncio_api.rst | 2 +- docs/asyncio_quick.rst | 2 +- docs/utils.rst | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/asyncio_api.rst b/docs/asyncio_api.rst index 1a0a2ee..a1985ac 100644 --- a/docs/asyncio_api.rst +++ b/docs/asyncio_api.rst @@ -309,7 +309,7 @@ Decorators result_signature='s', result_args_names=('uppercased', ) # This is optional but # makes arguments have names in - # instrospection data. + # introspection data. ) async def upper(self, str_to_up: str) -> str: return str_to_up.upper() diff --git a/docs/asyncio_quick.rst b/docs/asyncio_quick.rst index 363d09e..c035164 100644 --- a/docs/asyncio_quick.rst +++ b/docs/asyncio_quick.rst @@ -157,7 +157,7 @@ Example: :: result_signature='s', result_args_names=('uppercased', ) # This is optional but # makes arguments have names in - # instrospection data. + # introspection data. ) async def upper(self, str_to_up: str) -> str: return str_to_up.upper() diff --git a/docs/utils.rst b/docs/utils.rst index 851315e..39ae835 100644 --- a/docs/utils.rst +++ b/docs/utils.rst @@ -48,7 +48,7 @@ Parsing utilities Parse data from :py:meth:`interfaces_added ` signal. Takes an iterable of D-Bus interface classes (or a single class) and the signal data. - Returns the path of removed object andthe class of the added object. + Returns the path of removed object and the class of the added object. (if it matched one of passed interface classes) :param Iterable[DbusInterfaceBaseAsync] interfaces: Possible interfaces that were removed. From d6642f7f0d8937806493ed07bc0001a3412019e2 Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sat, 9 Mar 2024 14:48:01 +0500 Subject: [PATCH 081/188] Revert "Use ParamSpec for DbusMethodAsync instead of masking it" This reverts commit 72e9d52f91a27e499a9b0c2890d307c13496939e. The issue is that jedi does not support the ParamSpec yet. This will have a negative effect on developer experience because the D-Bus method arguments will no longer be showed when writing a method call. See: https://github.com/davidhalter/jedi/issues/1812 --- src/sdbus/dbus_proxy_async_method.py | 87 +++++++--------------------- test/test_sdbus_async_bad_class.py | 2 +- test/test_typing.py | 7 ++- 3 files changed, 27 insertions(+), 69 deletions(-) diff --git a/src/sdbus/dbus_proxy_async_method.py b/src/sdbus/dbus_proxy_async_method.py index 97e671d..d326674 100644 --- a/src/sdbus/dbus_proxy_async_method.py +++ b/src/sdbus/dbus_proxy_async_method.py @@ -22,7 +22,7 @@ from contextvars import ContextVar, copy_context from inspect import iscoroutinefunction from types import FunctionType -from typing import TYPE_CHECKING, Generic, TypeVar, cast, overload +from typing import TYPE_CHECKING, cast, overload from weakref import ref as weak_ref from .dbus_common_elements import ( @@ -36,27 +36,14 @@ from .sd_bus_internals import DbusNoReplyFlag if TYPE_CHECKING: - from typing import ( - Any, - Callable, - Coroutine, - Optional, - Sequence, - Type, - Union, - ) - - from typing_extensions import Concatenate, ParamSpec + from typing import Any, Callable, Optional, Sequence, Type, TypeVar, Union from .dbus_proxy_async_interface_base import DbusInterfaceBaseAsync from .sd_bus_internals import SdBusMessage - TDBI = TypeVar("TDBI", bound=DbusInterfaceBaseAsync) - P = ParamSpec("P") + T = TypeVar('T') else: - P = TypeVar("P") - -TR = TypeVar("TR") + T = None CURRENT_MESSAGE: ContextVar[SdBusMessage] = ContextVar('CURRENT_MESSAGE') @@ -65,18 +52,14 @@ def get_current_message() -> SdBusMessage: return CURRENT_MESSAGE.get() -class DbusMethodAsync( - DbusMethodCommon, - DbusSomethingAsync, - Generic[P, TR], -): +class DbusMethodAsync(DbusMethodCommon, DbusSomethingAsync): @overload def __get__( self, obj: None, obj_class: Type[DbusInterfaceBaseAsync], - ) -> DbusMethodAsync[P, TR]: + ) -> DbusMethodAsync: ... @overload @@ -84,14 +67,14 @@ def __get__( self, obj: DbusInterfaceBaseAsync, obj_class: Type[DbusInterfaceBaseAsync], - ) -> DbusMethodAsyncBaseBind[P, TR]: + ) -> Callable[..., Any]: ... def __get__( self, obj: Optional[DbusInterfaceBaseAsync], obj_class: Optional[Type[DbusInterfaceBaseAsync]] = None, - ) -> Union[DbusMethodAsyncBaseBind[P, TR], DbusMethodAsync[P, TR]]: + ) -> Union[Callable[..., Any], DbusMethodAsync]: if obj is not None: dbus_meta = obj._dbus if isinstance(dbus_meta, DbusRemoteObjectMeta): @@ -102,22 +85,16 @@ def __get__( return self -class DbusMethodAsyncBaseBind( - DbusBindedAsync, - Generic[P, TR], -): +class DbusMethodAsyncBaseBind(DbusBindedAsync): - def __call__( - *args: P.args, - **kwargs: P.kwargs, - ) -> Coroutine[Any, Any, TR]: + def __call__(self, *args: Any, **kwargs: Any) -> Any: raise NotImplementedError -class DbusMethodAsyncProxyBind(DbusMethodAsyncBaseBind[P, TR]): +class DbusMethodAsyncProxyBind(DbusMethodAsyncBaseBind): def __init__( self, - dbus_method: DbusMethodAsync[P, TR], + dbus_method: DbusMethodAsync, proxy_meta: DbusRemoteObjectMeta, ): self.dbus_method = dbus_method @@ -134,7 +111,7 @@ async def _dbus_async_call(self, call_message: SdBusMessage) -> Any: async def _no_reply() -> None: return None - def __call__(self, *args: P.args, **kwargs: P.kwargs) -> Any: + def __call__(self, *args: Any, **kwargs: Any) -> Any: bus = self.proxy_meta.attached_bus dbus_method = self.dbus_method @@ -168,10 +145,10 @@ def __call__(self, *args: P.args, **kwargs: P.kwargs) -> Any: return self._dbus_async_call(new_call_message) -class DbusMethodAsyncLocalBind(DbusMethodAsyncBaseBind[P, TR]): +class DbusMethodAsyncLocalBind(DbusMethodAsyncBaseBind): def __init__( self, - dbus_method: DbusMethodAsync[P, TR], + dbus_method: DbusMethodAsync, local_object: DbusInterfaceBaseAsync, ): self.dbus_method = dbus_method @@ -179,7 +156,7 @@ def __init__( self.__doc__ = dbus_method.__doc__ - def __call__(self, *args: P.args, **kwargs: P.kwargs) -> Any: + def __call__(self, *args: Any, **kwargs: Any) -> Any: local_object = self.local_object_ref() if local_object is None: raise RuntimeError("Local object no longer exists!") @@ -262,24 +239,14 @@ def dbus_method_async( result_args_names: Optional[Sequence[str]] = None, input_args_names: Optional[Sequence[str]] = None, method_name: Optional[str] = None, -) -> Callable[ - [Callable[ - Concatenate[TDBI, P], - Coroutine[Any, Any, TR]]], - DbusMethodAsync[P, TR], -]: +) -> Callable[[T], T]: assert not isinstance(input_signature, FunctionType), ( "Passed function to decorator directly. " "Did you forget () round brackets?" ) - def dbus_method_decorator( - original_method: Callable[ - Concatenate[TDBI, P], - Coroutine[Any, Any, TR] - ], - ) -> DbusMethodAsync[P, TR]: + def dbus_method_decorator(original_method: T) -> T: assert isinstance(original_method, FunctionType) assert iscoroutinefunction(original_method), ( "Expected coroutine function. ", @@ -295,25 +262,15 @@ def dbus_method_decorator( flags=flags, ) - return new_wrapper + return cast(T, new_wrapper) return dbus_method_decorator -def dbus_method_async_override( -) -> Callable[ - [Callable[ - Concatenate[TDBI, P], - Coroutine[Any, Any, TR]]], - DbusMethodAsync[P, TR], -]: +def dbus_method_async_override() -> Callable[[T], T]: def new_decorator( - new_function: Callable[ - Concatenate[TDBI, P], - Coroutine[Any, Any, TR] - ], - ) -> DbusMethodAsync[P, TR]: - return cast(DbusMethodAsync[P, TR], DbusOverload(new_function)) + new_function: T) -> T: + return cast(T, DbusOverload(new_function)) return new_decorator diff --git a/test/test_sdbus_async_bad_class.py b/test/test_sdbus_async_bad_class.py index 9aa8b95..a8b345a 100644 --- a/test/test_sdbus_async_bad_class.py +++ b/test/test_sdbus_async_bad_class.py @@ -163,7 +163,7 @@ def test_bad_subclass(self) -> None: with self.assertRaises(TypeError): class TestInheritence(TestInterface): - async def test_int(self) -> int: # type: ignore[override] + async def test_int(self) -> int: return 2 with self.assertRaises(TypeError): diff --git a/test/test_typing.py b/test/test_typing.py index 7feb9ef..a84b872 100644 --- a/test/test_typing.py +++ b/test/test_typing.py @@ -160,9 +160,10 @@ async def check_async_element_class_access_typing() -> None: test_list: List[str] = [] - test_list.append( - TestTypingAsync.get_str_list_method.method_name - ) + # TODO: Fix dbus async method typing + # test_list.append( + # TestTypingAsync.get_str_list_method.method_name + # ) test_list.append( TestTypingAsync.str_list_property.property_name ) From edf2a5c5c98391b94d83f45f7e4507408e250d49 Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sat, 9 Mar 2024 20:17:58 +0500 Subject: [PATCH 082/188] Rework blocking interface collision resolution to use __mro__ New algorithm uses __mro__ to track the declared D-Bus interfaces and their attributes. This provides several advantages such as allowing classes that have two bases with shared parent interface as well as less copying of attribute maps. --- src/sdbus/dbus_common_elements.py | 9 +- src/sdbus/dbus_proxy_async_interface_base.py | 5 +- src/sdbus/dbus_proxy_sync_interface_base.py | 200 ++++++++++++------- src/sdbus/dbus_proxy_sync_interfaces.py | 5 +- test/test_sdbus_block.py | 10 +- test/test_sdbus_block_bad_class.py | 77 +++++-- 6 files changed, 206 insertions(+), 100 deletions(-) diff --git a/src/sdbus/dbus_common_elements.py b/src/sdbus/dbus_common_elements.py index 42e1efd..d785dbc 100644 --- a/src/sdbus/dbus_common_elements.py +++ b/src/sdbus/dbus_common_elements.py @@ -40,10 +40,12 @@ Sequence, Set, Tuple, + Type, TypeVar, ) T = TypeVar('T') + SelfMeta = TypeVar('SelfMeta', bound="DbusInterfaceMetaCommon") from .sd_bus_internals import SdBus, SdBusInterface @@ -62,12 +64,12 @@ class DbusSomethingSync(DbusSomethingCommon): class DbusInterfaceMetaCommon(type): - def __new__(cls, name: str, + def __new__(cls: Type[SelfMeta], name: str, bases: Tuple[type, ...], namespace: Dict[str, Any], interface_name: Optional[str] = None, serving_enabled: bool = True, - ) -> DbusInterfaceMetaCommon: + ) -> SelfMeta: if interface_name is not None: try: assert is_interface_name_valid(interface_name), ( @@ -338,7 +340,8 @@ def __init__(self) -> None: class DbusClassMeta: - def __init__(self) -> None: + def __init__(self, interface_name: str) -> None: + self.interface_name = interface_name self.dbus_member_to_python_attr: Dict[str, str] = {} self.dbus_interfaces_names: Set[str] = set() self.python_attr_to_dbus_member: Dict[str, str] = {} diff --git a/src/sdbus/dbus_proxy_async_interface_base.py b/src/sdbus/dbus_proxy_async_interface_base.py index 3dc6ff7..7a919ee 100644 --- a/src/sdbus/dbus_proxy_async_interface_base.py +++ b/src/sdbus/dbus_proxy_async_interface_base.py @@ -71,7 +71,8 @@ def __new__(cls, name: str, serving_enabled: bool = True, ) -> DbusInterfaceMetaAsync: - dbus_class_meta = DbusClassMeta() + dbus_class_meta = DbusClassMeta(interface_name or "") + if interface_name is not None and serving_enabled: dbus_class_meta.dbus_interfaces_names.add(interface_name) @@ -204,7 +205,7 @@ def __new__(cls, name: str, serving_enabled, ) - return cast(DbusInterfaceMetaAsync, new_cls) + return new_cls class DbusInterfaceBaseAsync(metaclass=DbusInterfaceMetaAsync): diff --git a/src/sdbus/dbus_proxy_sync_interface_base.py b/src/sdbus/dbus_proxy_sync_interface_base.py index b38688f..777e765 100644 --- a/src/sdbus/dbus_proxy_sync_interface_base.py +++ b/src/sdbus/dbus_proxy_sync_interface_base.py @@ -19,7 +19,9 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA from __future__ import annotations -from typing import TYPE_CHECKING, cast +from itertools import chain +from typing import TYPE_CHECKING +from weakref import WeakKeyDictionary, WeakValueDictionary from .dbus_common_elements import ( DbusClassMeta, @@ -32,12 +34,98 @@ from .dbus_proxy_sync_property import DbusPropertySync if TYPE_CHECKING: - from typing import Any, ClassVar, Dict, Optional, Tuple + from typing import ( + Any, + Dict, + Iterable, + Iterator, + Optional, + Set, + Tuple, + Type, + ) from .sd_bus_internals import SdBus +DBUS_CLASS_TO_META: WeakKeyDictionary[ + type, DbusClassMeta] = WeakKeyDictionary() +DBUS_INTERFACE_NAME_TO_CLASS: WeakValueDictionary[ + str, DbusInterfaceMetaSync] = WeakValueDictionary() + + class DbusInterfaceMetaSync(DbusInterfaceMetaCommon): + + @staticmethod + def check_collisions( + new_class_name: str, + attr_names: Set[str], + reserved_attr_names: Set[str], + ) -> None: + + possible_collisions = attr_names & reserved_attr_names + if possible_collisions: + raise ValueError( + f"Interface {new_class_name!r} redefines reserved " + f"D-Bus attribute names: {possible_collisions!r}" + ) + + @staticmethod + def collect_dbus_to_python_attr_names( + new_class_name: str, + base_classes: Iterable[type], + ) -> Set[str]: + all_python_dbus_attrs: Set[str] = set() + possible_collisions: Set[str] = set() + + for c in base_classes: + dbus_meta = DBUS_CLASS_TO_META.get(c) + if dbus_meta is None: + continue + + base_python_dbus_attrs = set( + dbus_meta.python_attr_to_dbus_member.keys() + ) + + possible_collisions.update( + base_python_dbus_attrs & all_python_dbus_attrs + ) + + all_python_dbus_attrs.update( + base_python_dbus_attrs + ) + + if possible_collisions: + raise ValueError( + f"Interface {new_class_name!r} has a reserved D-Bus " + f"attribute name collision: {possible_collisions!r}" + ) + + return all_python_dbus_attrs + + @staticmethod + def map_dbus_elements( + attr_name: str, + attr: Any, + meta: DbusClassMeta, + ) -> None: + if not isinstance(attr, DbusSomethingCommon): + return + + if isinstance(attr, DbusSomethingAsync): + raise TypeError( + f"Can't mix async methods in sync interface: {attr_name!r}" + ) + + if isinstance(attr, DbusMethodSync): + meta.dbus_member_to_python_attr[attr.method_name] = attr_name + meta.python_attr_to_dbus_member[attr_name] = attr.method_name + elif isinstance(attr, DbusPropertySync): + meta.dbus_member_to_python_attr[attr.property_name] = attr_name + meta.python_attr_to_dbus_member[attr_name] = attr.property_name + else: + raise TypeError(f"Unknown D-Bus element: {attr!r}") + def __new__(cls, name: str, bases: Tuple[type, ...], namespace: Dict[str, Any], @@ -45,92 +133,38 @@ def __new__(cls, name: str, serving_enabled: bool = True, ) -> DbusInterfaceMetaSync: - dbus_class_meta = DbusClassMeta() - if interface_name is not None and serving_enabled: - dbus_class_meta.dbus_interfaces_names.add(interface_name) - - for attr_name, attr in namespace.items(): - if not isinstance(attr, DbusSomethingCommon): - continue - - if isinstance(attr, DbusSomethingAsync): - raise TypeError( - f"Can't mix async methods in sync interface: {attr_name!r}" - ) - - if not serving_enabled: - continue + if interface_name in DBUS_INTERFACE_NAME_TO_CLASS: + raise ValueError( + f"D-Bus interface of the name {interface_name!r} was " + "already created." + ) - if isinstance(attr, DbusMethodSync): - dbus_class_meta.dbus_member_to_python_attr[ - attr.method_name] = attr_name - dbus_class_meta.python_attr_to_dbus_member[ - attr_name] = attr.method_name - elif isinstance(attr, DbusPropertySync): - dbus_class_meta.dbus_member_to_python_attr[ - attr.property_name] = attr_name - dbus_class_meta.python_attr_to_dbus_member[ - attr_name] = attr.property_name - else: - raise TypeError(f"Unknown D-Bus element: {attr!r}") - - for base in bases: - if not issubclass(base, DbusInterfaceBase): - continue + all_mro_bases: Set[Type[Any]] = set( + chain.from_iterable((c.__mro__ for c in bases)) + ) + reserved_attr_names = cls.collect_dbus_to_python_attr_names( + name, all_mro_bases, + ) + cls.check_collisions(name, set(namespace.keys()), reserved_attr_names) - # Update interfaces names set - base_interfaces_names = base._dbus_meta.dbus_interfaces_names - if dbus_interface_name_collision := ( - dbus_class_meta.dbus_interfaces_names - & base_interfaces_names - ): - raise TypeError( - f"Interface {name!r} and {base!r} have interface name " - f"collision: {dbus_interface_name_collision}" - ) - else: - dbus_class_meta.dbus_interfaces_names.update( - base_interfaces_names - ) - - if dbus_member_collision := ( - dbus_class_meta.dbus_member_to_python_attr.keys() - & base._dbus_meta.dbus_member_to_python_attr.keys() - ): - raise TypeError( - f"Interface {name!r} and {base!r} have D-Bus member " - f"collision: {dbus_member_collision}" - ) - else: - dbus_class_meta.dbus_member_to_python_attr.update( - base._dbus_meta.dbus_member_to_python_attr - ) - - if python_attr_collision := ( - namespace.keys() - & base._dbus_meta.python_attr_to_dbus_member.keys() - ): - raise TypeError( - f"Interface {name!r} and {base!r} have Python attribute " - f"collision: {python_attr_collision}" - ) - else: - dbus_class_meta.python_attr_to_dbus_member.update( - base._dbus_meta.python_attr_to_dbus_member - ) - - namespace['_dbus_meta'] = dbus_class_meta new_cls = super().__new__( cls, name, bases, namespace, interface_name, serving_enabled, ) - return cast(DbusInterfaceMetaSync, new_cls) + if interface_name is not None: + dbus_class_meta = DbusClassMeta(interface_name) + DBUS_CLASS_TO_META[new_cls] = dbus_class_meta + DBUS_INTERFACE_NAME_TO_CLASS[interface_name] = new_cls + + for attr_name, attr in namespace.items(): + cls.map_dbus_elements(attr_name, attr, dbus_class_meta) + + return new_cls class DbusInterfaceBase(metaclass=DbusInterfaceMetaSync): - _dbus_meta: ClassVar[DbusClassMeta] def __init__( self, @@ -139,3 +173,15 @@ def __init__( bus: Optional[SdBus] = None, ): self._dbus = DbusRemoteObjectMeta(service_name, object_path, bus) + + @classmethod + def _dbus_iter_interfaces_meta( + cls, + ) -> Iterator[Tuple[str, DbusClassMeta]]: + + for base in cls.__mro__: + meta = DBUS_CLASS_TO_META.get(base) + if meta is None: + continue + + yield meta.interface_name, meta diff --git a/src/sdbus/dbus_proxy_sync_interfaces.py b/src/sdbus/dbus_proxy_sync_interfaces.py index b0d88eb..f8f3dbb 100644 --- a/src/sdbus/dbus_proxy_sync_interfaces.py +++ b/src/sdbus/dbus_proxy_sync_interfaces.py @@ -70,12 +70,11 @@ def properties_get_all_dict( ) -> Dict[str, Any]: properties: Dict[str, Any] = {} - for interface_name in self._dbus_meta.dbus_interfaces_names: + for interface_name, meta in self._dbus_iter_interfaces_meta(): dbus_properties_data = self._properties_get_all(interface_name) for member_name, variant in dbus_properties_data.items(): try: - python_name = self._dbus_meta.dbus_member_to_python_attr[ - member_name] + python_name = meta.dbus_member_to_python_attr[member_name] except KeyError: if on_unknown_member == 'error': raise diff --git a/test/test_sdbus_block.py b/test/test_sdbus_block.py index a3f9f66..7618d81 100644 --- a/test/test_sdbus_block.py +++ b/test/test_sdbus_block.py @@ -53,9 +53,13 @@ def test_sync(self) -> None: self.assertTrue(s.get_name_owner('org.example.test')) with self.subTest('Test dbus to python name map'): - self.assertEqual( - 'features', - s._dbus_meta.dbus_member_to_python_attr['Features'], + self.assertTrue( + any( + "Features" in meta.dbus_member_to_python_attr + for meta in ( + meta for _, meta in s._dbus_iter_interfaces_meta() + ) + ) ) with self.subTest('Test properties_get_all_dict'): diff --git a/test/test_sdbus_block_bad_class.py b/test/test_sdbus_block_bad_class.py index 1b703c5..14855e7 100644 --- a/test/test_sdbus_block_bad_class.py +++ b/test/test_sdbus_block_bad_class.py @@ -19,6 +19,7 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA from __future__ import annotations +from gc import collect from unittest import TestCase from unittest import main as unittest_main @@ -42,14 +43,14 @@ def test_property(self) -> str: class TestBadDbusClass(TestCase): def test_method_name_override(self) -> None: - with self.subTest("Method override"), self.assertRaises(TypeError): + with self.subTest("Method override"), self.assertRaises(ValueError): class BadMethodOverrideClass(GoodDbusInterface): def test_method(self) -> None: return with self.subTest("D-Bus method override"), self.assertRaises( - TypeError + ValueError ): class BadDbusMethodOverrideClass(GoodDbusInterface): @@ -57,14 +58,14 @@ class BadDbusMethodOverrideClass(GoodDbusInterface): def test_method(self) -> None: return - with self.subTest("Property override"), self.assertRaises(TypeError): + with self.subTest("Property override"), self.assertRaises(ValueError): class BadPropertyOverrideClass(GoodDbusInterface): def test_property(self) -> str: # type: ignore return "override" with self.subTest("D-Bus property override"), self.assertRaises( - TypeError + ValueError ): class BadDbusPropertyOverrideClass(GoodDbusInterface): @@ -84,14 +85,11 @@ class NonInterface(GoodDbusInterface): def do_work(self) -> None: ... - class NewExampleInterface( - DbusInterfaceCommon, - interface_name="org.example.test", - ): - ... - - with self.subTest("Collision"), self.assertRaises(TypeError): - class Collision(NewExampleInterface, GoodDbusInterface): + with self.subTest("Collision"), self.assertRaises(ValueError): + class NewExampleInterface( + DbusInterfaceCommon, + interface_name="org.example.test", + ): ... def test_bad_class_names(self) -> None: @@ -145,6 +143,61 @@ class NoInterfaceName(DbusInterfaceCommon): def example(self) -> None: ... + def test_shared_parent_class(self) -> None: + class One(GoodDbusInterface): + ... + + class Two(GoodDbusInterface): + ... + + class Shared(One, Two): + ... + + def test_combined_collision(self) -> None: + + class One( + DbusInterfaceCommon, + interface_name="org.example.foo", + ): + @dbus_method() + def example(self) -> None: + ... + + class Two( + DbusInterfaceCommon, + interface_name="org.example.bar", + ): + @dbus_method() + def example(self) -> None: + ... + + with self.assertRaisesRegex(ValueError, "collision"): + class Combined(One, Two): + ... + + def test_class_cleanup(self) -> None: + class One( + DbusInterfaceCommon, + interface_name="org.example.foo1", + ): + ... + + with self.assertRaises(ValueError): + class Two( + DbusInterfaceCommon, + interface_name="org.example.foo1", + ): + ... + + del One + collect() # Let weak refs be processed + + class After( + DbusInterfaceCommon, + interface_name="org.example.foo1", + ): + ... + if __name__ == "__main__": unittest_main() From 3a98df550792dbbbc1d5798ffa86c43beeb10cf2 Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sat, 9 Mar 2024 20:22:38 +0500 Subject: [PATCH 083/188] Typo fix constist ==> consist --- src/sdbus/dbus_common_elements.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sdbus/dbus_common_elements.py b/src/sdbus/dbus_common_elements.py index d785dbc..54ad3ce 100644 --- a/src/sdbus/dbus_common_elements.py +++ b/src/sdbus/dbus_common_elements.py @@ -76,7 +76,7 @@ def __new__(cls: Type[SelfMeta], name: str, f"Invalid interface name: \"{interface_name}\"; " 'Interface names must be composed of 2 or more elements ' 'separated by a dot \'.\' character. All elements must ' - 'contain at least one character, constist of ASCII ' + 'contain at least one character, consist of ASCII ' 'characters, first character must not be digit and ' 'length must not exceed 255 characters.' ) From a3c8e2e2cac7cf41bad44e9d1418f94ee6163531 Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sat, 9 Mar 2024 20:23:20 +0500 Subject: [PATCH 084/188] Enable --pretty mode for mypy This will printi entire lines there errors occur. --- tools/run_py_linters.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/run_py_linters.py b/tools/run_py_linters.py index a9e858e..30af375 100755 --- a/tools/run_py_linters.py +++ b/tools/run_py_linters.py @@ -49,7 +49,7 @@ def run_mypy() -> None: print('Running mypy on all modules') run( args=( - 'mypy', '--strict', + 'mypy', '--strict', '--pretty', '--cache-dir', mypy_cache_dir, '--python-version', '3.8', '--namespace-packages', From 8f86747236fe8a1cb6195b98eb1082d19267d0bf Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sun, 10 Mar 2024 17:25:35 +0500 Subject: [PATCH 085/188] Rework async interface collision detection to use __mro__ New algorithm uses __mro__ to track the declared D-Bus interfaces and their attributes. This provides several advantages such as allowing classes that have two bases with shared parent interface as well as less copying of attribute maps. --- src/sdbus/dbus_common_elements.py | 19 +- src/sdbus/dbus_common_funcs.py | 4 +- src/sdbus/dbus_proxy_async_interface_base.py | 320 ++++++++++++------- src/sdbus/dbus_proxy_async_interfaces.py | 4 +- src/sdbus/dbus_proxy_async_method.py | 4 +- src/sdbus/dbus_proxy_async_property.py | 4 +- src/sdbus/utils.py | 59 ++-- test/test_low_level_errors.py | 2 +- test/test_object_manager.py | 2 +- test/test_sdbus_async.py | 25 +- test/test_sdbus_async_bad_class.py | 66 +++- test/test_sdbus_async_introspection.py | 8 +- 12 files changed, 346 insertions(+), 171 deletions(-) diff --git a/src/sdbus/dbus_common_elements.py b/src/sdbus/dbus_common_elements.py index 54ad3ce..1f66d8b 100644 --- a/src/sdbus/dbus_common_elements.py +++ b/src/sdbus/dbus_common_elements.py @@ -38,7 +38,6 @@ List, Optional, Sequence, - Set, Tuple, Type, TypeVar, @@ -300,20 +299,25 @@ class DbusBindedSync: ... -class DbusOverload: - def __init__(self, original: T): - self.original = original - self.setter_overload: Optional[Callable[[Any, T], None]] = None +class DbusMethodOverride: + def __init__(self, override_method: T): + self.override_method = override_method + + +class DbusPropertyOverride: + def __init__(self, getter_override: T): + self.getter_override = getter_override + self.setter_override: Optional[Callable[[Any, T], None]] = None self.is_setter_public = True def setter(self, new_setter: Optional[Callable[[Any, T], None]]) -> None: - self.setter_overload = new_setter + self.setter_override = new_setter def setter_private( self, new_setter: Optional[Callable[[Any, T], None]], ) -> None: - self.setter_overload = new_setter + self.setter_override = new_setter self.is_setter_public = False @@ -343,5 +347,4 @@ class DbusClassMeta: def __init__(self, interface_name: str) -> None: self.interface_name = interface_name self.dbus_member_to_python_attr: Dict[str, str] = {} - self.dbus_interfaces_names: Set[str] = set() self.python_attr_to_dbus_member: Dict[str, str] = {} diff --git a/src/sdbus/dbus_common_funcs.py b/src/sdbus/dbus_common_funcs.py index 6987396..7e7baf7 100644 --- a/src/sdbus/dbus_common_funcs.py +++ b/src/sdbus/dbus_common_funcs.py @@ -37,7 +37,7 @@ ) if TYPE_CHECKING: - from typing import Any, Dict, Generator, Iterator, Literal, Tuple + from typing import Any, Dict, Generator, Iterator, Literal, Mapping, Tuple from .sd_bus_internals import SdBus @@ -166,7 +166,7 @@ def _check_sync_in_async_env() -> bool: def _parse_properties_vardict( - properties_name_map: Dict[str, str], + properties_name_map: Mapping[str, str], properties_vardict: Dict[str, Tuple[str, Any]], on_unknown_member: Literal['error', 'ignore', 'reuse'], ) -> Dict[str, Any]: diff --git a/src/sdbus/dbus_proxy_async_interface_base.py b/src/sdbus/dbus_proxy_async_interface_base.py index 7a919ee..3f48723 100644 --- a/src/sdbus/dbus_proxy_async_interface_base.py +++ b/src/sdbus/dbus_proxy_async_interface_base.py @@ -21,15 +21,18 @@ from copy import copy from inspect import getmembers +from itertools import chain from types import MethodType from typing import TYPE_CHECKING, Any, Callable, cast from warnings import warn +from weakref import WeakKeyDictionary, WeakValueDictionary from .dbus_common_elements import ( DbusClassMeta, DbusInterfaceMetaCommon, DbusLocalObjectMeta, - DbusOverload, + DbusMethodOverride, + DbusPropertyOverride, DbusRemoteObjectMeta, DbusSomethingAsync, DbusSomethingCommon, @@ -46,8 +49,9 @@ if TYPE_CHECKING: from typing import ( - ClassVar, Dict, + Iterable, + Iterator, List, Optional, Set, @@ -61,160 +65,252 @@ from .sd_bus_internals import SdBus Self = TypeVar('Self', bound="DbusInterfaceBaseAsync") + DbusOverride = Union[DbusMethodOverride, DbusPropertyOverride] + + +DBUS_CLASS_TO_META: WeakKeyDictionary[ + type, DbusClassMeta] = WeakKeyDictionary() +DBUS_INTERFACE_NAME_TO_CLASS: WeakValueDictionary[ + str, DbusInterfaceMetaAsync] = WeakValueDictionary() class DbusInterfaceMetaAsync(DbusInterfaceMetaCommon): - def __new__(cls, name: str, - bases: Tuple[type, ...], - namespace: Dict[str, Any], - interface_name: Optional[str] = None, - serving_enabled: bool = True, - ) -> DbusInterfaceMetaAsync: - dbus_class_meta = DbusClassMeta(interface_name or "") + @staticmethod + def process_dbus_method_override( + override_attr_name: str, + override: DbusMethodOverride, + mro_dbus_elements: Dict[str, DbusSomethingAsync], + ) -> DbusMethodAsync: + try: + original_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): + raise TypeError( + f"Expected {DbusMethodAsync!r} got {original_method!r} " + f"under name {override_attr_name!r}" + ) + + new_method = copy(original_method) + new_method.original_method = cast(MethodType, override.override_method) + return new_method + + @staticmethod + def process_dbus_property_override( + override_attr_name: str, + override: DbusPropertyOverride, + mro_dbus_elements: Dict[str, DbusSomethingAsync], + ) -> DbusPropertyAsync[Any]: + try: + original_property = mro_dbus_elements[override_attr_name] + except KeyError: + raise ValueError( + f"No D-Bus property {override_attr_name!r} found " + f"to override." + ) + + if not isinstance(original_property, DbusPropertyAsync): + raise TypeError( + f"Expected {DbusMethodAsync!r} got {original_property!r} " + f"under name {override_attr_name!r}" + ) + + new_property = copy(original_property) + new_property.property_getter = cast( + Callable[[DbusInterfaceBaseAsync], Any], + override.getter_override + ) + if override.setter_override is not None: + new_property.property_setter = override.setter_override + new_property.property_setter_is_public = override.is_setter_public + + return new_property - if interface_name is not None and serving_enabled: - dbus_class_meta.dbus_interfaces_names.add(interface_name) + @classmethod + def check_collisions( + cls, + new_class_name: str, + namespace: Dict[str, Any], + mro_dbus_elements: Dict[str, DbusSomethingAsync], + ) -> None: - overrides: Dict[str, DbusOverload] = {} - unresolved_collisions: Set[str] = set() + possible_collisions = namespace.keys() & mro_dbus_elements.keys() + new_overrides: Dict[str, DbusSomethingAsync] = {} for attr_name, attr in namespace.items(): - if isinstance(attr, DbusOverload): - overrides[attr_name] = attr + if isinstance(attr, DbusMethodOverride): + new_overrides[attr_name] = cls.process_dbus_method_override( + attr_name, + attr, + mro_dbus_elements, + ) + possible_collisions.remove(attr_name) + elif isinstance(attr, DbusPropertyOverride): + new_overrides[attr_name] = cls.process_dbus_property_override( + attr_name, + attr, + mro_dbus_elements, + ) + possible_collisions.remove(attr_name) + else: continue - if not isinstance(attr, DbusSomethingCommon): - continue + if possible_collisions: + raise ValueError( + f"Interface {new_class_name!r} redefines reserved " + f"D-Bus attribute names: {possible_collisions!r}" + ) + + namespace.update(new_overrides) - if isinstance(attr, DbusSomethingSync): + @staticmethod + def extract_dbus_elements( + dbus_class: type, + dbus_meta: DbusClassMeta, + ) -> Dict[str, DbusSomethingAsync]: + dbus_elements_map: Dict[str, DbusSomethingAsync] = {} + + for attr_name in dbus_meta.python_attr_to_dbus_member.keys(): + dbus_element = dbus_class.__dict__.get(attr_name) + if not isinstance(dbus_element, DbusSomethingAsync): raise TypeError( - "Can't mix blocking methods in " - f"async interface: {attr_name!r}" + f"Expected async D-Bus element, got {dbus_element!r} " + f"in class {dbus_class!r}" ) - if not serving_enabled: - continue + dbus_elements_map[attr_name] = dbus_element - if isinstance(attr, DbusMethodAsync): - dbus_class_meta.dbus_member_to_python_attr[ - attr.method_name] = attr_name - dbus_class_meta.python_attr_to_dbus_member[ - attr_name] = attr.method_name - elif isinstance(attr, DbusPropertyAsync): - dbus_class_meta.dbus_member_to_python_attr[ - attr.property_name] = attr_name - dbus_class_meta.python_attr_to_dbus_member[ - attr_name] = attr.property_name - elif isinstance(attr, DbusSignalAsync): - dbus_class_meta.dbus_member_to_python_attr[ - attr.signal_name] = attr_name - dbus_class_meta.python_attr_to_dbus_member[ - attr_name] = attr.signal_name - else: - raise TypeError(f"Unknown D-Bus element: {attr!r}") + return dbus_elements_map - for base in bases: - if not issubclass(base, DbusInterfaceBaseAsync): + @classmethod + def map_mro_dbus_elements( + cls, + new_class_name: str, + base_classes: Iterable[type], + ) -> Dict[str, DbusSomethingAsync]: + all_python_dbus_map: Dict[str, DbusSomethingAsync] = {} + possible_collisions: Set[str] = set() + + for c in base_classes: + dbus_meta = DBUS_CLASS_TO_META.get(c) + if dbus_meta is None: continue - # Update interfaces names set - base_interfaces_names = base._dbus_meta.dbus_interfaces_names - if dbus_interface_name_collision := ( - dbus_class_meta.dbus_interfaces_names - & base_interfaces_names - ): - raise TypeError( - f"Interface {name!r} and {base!r} have interface name " - f"collision: {dbus_interface_name_collision}" - ) - else: - dbus_class_meta.dbus_interfaces_names.update( - base_interfaces_names - ) + base_dbus_elements = cls.extract_dbus_elements(c, dbus_meta) - if dbus_member_collision := ( - dbus_class_meta.dbus_member_to_python_attr.keys() - & base._dbus_meta.dbus_member_to_python_attr.keys() - ): - raise TypeError( - f"Interface {name!r} and {base!r} have D-Bus member " - f"collision: {dbus_member_collision}" - ) - else: - dbus_class_meta.dbus_member_to_python_attr.update( - base._dbus_meta.dbus_member_to_python_attr - ) + possible_collisions.update( + base_dbus_elements.keys() & all_python_dbus_map.keys() + ) - for collision_name in ( - namespace.keys() - & base._dbus_meta.python_attr_to_dbus_member.keys() - ): - try: - override = overrides.pop(collision_name) - except KeyError: - unresolved_collisions.add(collision_name) - continue + all_python_dbus_map.update( + base_dbus_elements + ) - super_element = getattr(base, collision_name) - dbus_element_override: DbusSomethingAsync - if isinstance(super_element, DbusMethodAsync): - dbus_element_override = copy(super_element) - dbus_element_override.original_method = cast( - MethodType, override.original) - elif isinstance(super_element, DbusPropertyAsync): - dbus_element_override = copy(super_element) - dbus_element_override.property_getter = cast( - Callable[[DbusInterfaceBaseAsync], Any], - override.original) - if override.setter_overload is not None: - dbus_element_override.property_setter = ( - override.setter_overload - ) - dbus_element_override.property_setter_is_public = ( - override.is_setter_public - ) - else: - raise TypeError( - f"Unknown override {collision_name!r} " - f"with {super_element!r}" - ) + if possible_collisions: + raise ValueError( + f"Interface {new_class_name!r} has a reserved D-Bus " + f"attribute name collision: {possible_collisions!r}" + ) - namespace[collision_name] = dbus_element_override + return all_python_dbus_map - dbus_class_meta.python_attr_to_dbus_member.update( - base._dbus_meta.python_attr_to_dbus_member - ) + @staticmethod + def map_dbus_elements( + attr_name: str, + attr: Any, + meta: DbusClassMeta, + interface_name: str, + ) -> None: + if not isinstance(attr, DbusSomethingCommon): + return - if unresolved_collisions: + if isinstance(attr, DbusSomethingSync): raise TypeError( - f"Interface {name!r} and {base!r} have Python attribute " - f"collision: {unresolved_collisions}" + "Can't mix blocking methods in " + f"async interface: {attr_name!r}" ) - if overrides: - raise TypeError( - f"Interface {name!r} has unresolved overrides:", - set(overrides.keys()), + if attr.interface_name != interface_name: + return + + if isinstance(attr, DbusMethodAsync): + meta.dbus_member_to_python_attr[attr.method_name] = attr_name + meta.python_attr_to_dbus_member[attr_name] = attr.method_name + elif isinstance(attr, DbusPropertyAsync): + meta.dbus_member_to_python_attr[attr.property_name] = attr_name + meta.python_attr_to_dbus_member[attr_name] = attr.property_name + elif isinstance(attr, DbusSignalAsync): + meta.dbus_member_to_python_attr[attr.signal_name] = attr_name + meta.python_attr_to_dbus_member[attr_name] = attr.signal_name + else: + raise TypeError(f"Unknown D-Bus element: {attr!r}") + + def __new__(cls, name: str, + bases: Tuple[type, ...], + namespace: Dict[str, Any], + interface_name: Optional[str] = None, + serving_enabled: bool = True, + ) -> DbusInterfaceMetaAsync: + + if interface_name in DBUS_INTERFACE_NAME_TO_CLASS: + raise ValueError( + f"D-Bus interface of the name {interface_name!r} was " + "already created." ) - namespace['_dbus_meta'] = dbus_class_meta + all_mro_bases: Set[Type[Any]] = set( + chain.from_iterable((c.__mro__ for c in bases)) + ) + reserved_dbus_map = cls.map_mro_dbus_elements( + name, all_mro_bases, + ) + cls.check_collisions(name, namespace, reserved_dbus_map) + new_cls = super().__new__( cls, name, bases, namespace, interface_name, serving_enabled, ) + if interface_name is not None: + dbus_class_meta = DbusClassMeta(interface_name) + DBUS_CLASS_TO_META[new_cls] = dbus_class_meta + DBUS_INTERFACE_NAME_TO_CLASS[interface_name] = new_cls + + for attr_name, attr in namespace.items(): + cls.map_dbus_elements( + attr_name, + attr, + dbus_class_meta, + interface_name, + ) + return new_cls class DbusInterfaceBaseAsync(metaclass=DbusInterfaceMetaAsync): - _dbus_meta: ClassVar[DbusClassMeta] def __init__(self) -> None: self._dbus: Union[ DbusRemoteObjectMeta, DbusLocalObjectMeta] = DbusLocalObjectMeta() + @classmethod + def _dbus_iter_interfaces_meta( + cls, + ) -> Iterator[Tuple[str, DbusClassMeta]]: + + for base in cls.__mro__: + meta = DBUS_CLASS_TO_META.get(base) + if meta is None: + continue + + yield meta.interface_name, meta + async def start_serving(self, object_path: str, bus: Optional[SdBus] = None, diff --git a/src/sdbus/dbus_proxy_async_interfaces.py b/src/sdbus/dbus_proxy_async_interfaces.py index 6b1b78b..bb09810 100644 --- a/src/sdbus/dbus_proxy_async_interfaces.py +++ b/src/sdbus/dbus_proxy_async_interfaces.py @@ -88,13 +88,13 @@ async def properties_get_all_dict( properties: Dict[str, Any] = {} - for interface_name in self._dbus_meta.dbus_interfaces_names: + for interface_name, meta in self._dbus_iter_interfaces_meta(): dbus_properties_data = await self._properties_get_all( interface_name) properties.update( _parse_properties_vardict( - self._dbus_meta.dbus_member_to_python_attr, + meta.dbus_member_to_python_attr, dbus_properties_data, on_unknown_member, ) diff --git a/src/sdbus/dbus_proxy_async_method.py b/src/sdbus/dbus_proxy_async_method.py index d326674..d4c6138 100644 --- a/src/sdbus/dbus_proxy_async_method.py +++ b/src/sdbus/dbus_proxy_async_method.py @@ -28,7 +28,7 @@ from .dbus_common_elements import ( DbusBindedAsync, DbusMethodCommon, - DbusOverload, + DbusMethodOverride, DbusRemoteObjectMeta, DbusSomethingAsync, ) @@ -271,6 +271,6 @@ def dbus_method_async_override() -> Callable[[T], T]: def new_decorator( new_function: T) -> T: - return cast(T, DbusOverload(new_function)) + return cast(T, DbusMethodOverride(new_function)) return new_decorator diff --git a/src/sdbus/dbus_proxy_async_property.py b/src/sdbus/dbus_proxy_async_property.py index 2af1ce3..b6ebdee 100644 --- a/src/sdbus/dbus_proxy_async_property.py +++ b/src/sdbus/dbus_proxy_async_property.py @@ -26,8 +26,8 @@ from .dbus_common_elements import ( DbusBindedAsync, - DbusOverload, DbusPropertyCommon, + DbusPropertyOverride, DbusRemoteObjectMeta, DbusSomethingAsync, ) @@ -311,6 +311,6 @@ def dbus_property_async_override() -> Callable[ def new_decorator( new_property: Callable[[Any], T]) -> DbusPropertyAsync[T]: - return cast(DbusPropertyAsync[T], DbusOverload(new_property)) + return cast(DbusPropertyAsync[T], DbusPropertyOverride(new_property)) return new_decorator diff --git a/src/sdbus/utils.py b/src/sdbus/utils.py index cd7a42f..f2be439 100644 --- a/src/sdbus/utils.py +++ b/src/sdbus/utils.py @@ -22,7 +22,11 @@ from typing import TYPE_CHECKING from .dbus_common_funcs import _parse_properties_vardict -from .dbus_proxy_async_interface_base import DbusInterfaceBaseAsync +from .dbus_proxy_async_interface_base import ( + DBUS_CLASS_TO_META, + DBUS_INTERFACE_NAME_TO_CLASS, + DbusInterfaceBaseAsync, +) if TYPE_CHECKING: from typing import ( @@ -46,13 +50,17 @@ def parse_properties_changed( properties_changed_data: DBUS_PROPERTIES_CHANGED_TYPING, on_unknown_member: Literal['error', 'ignore', 'reuse'] = 'error', ) -> Dict[str, Any]: - changed_properties_data = properties_changed_data[1] + interface_name, changed_properties, invalidated_properties = ( + properties_changed_data + ) + + meta = DBUS_CLASS_TO_META[DBUS_INTERFACE_NAME_TO_CLASS[interface_name]] - for invalidated_property in properties_changed_data[2]: - changed_properties_data[invalidated_property] = ('0', None) + for invalidated_property in invalidated_properties: + changed_properties[invalidated_property] = ('0', None) return _parse_properties_vardict( - interface._dbus_meta.dbus_member_to_python_attr, + meta.dbus_member_to_python_attr, properties_changed_data[1], on_unknown_member, ) @@ -80,22 +88,15 @@ def _create_interfaces_map( ] = {} for interface in interfaces_iter: - if ( - isinstance(interface, DbusInterfaceBaseAsync) - ): - interfaces_to_class_map[ - frozenset(interface._dbus_meta.dbus_interfaces_names) - ] = type(interface) - elif ( - isinstance(interface, type) - and - issubclass(interface, DbusInterfaceBaseAsync) - ): - interfaces_to_class_map[ - frozenset(interface._dbus_meta.dbus_interfaces_names) - ] = interface - else: - raise TypeError('Expected D-Bus interface, got: ', interface) + interface_names_set = frozenset( + interface_name for interface_name, _ in + interface._dbus_iter_interfaces_meta() + if interface_name not in SKIP_INTERFACES + ) + interfaces_to_class_map[interface_names_set] = ( + interface if isinstance(interface, type) + else type(interface) + ) return interfaces_to_class_map @@ -131,8 +132,12 @@ def parse_interfaces_added( class_set = frozenset(properties_data.keys()) - SKIP_INTERFACES try: python_class = interfaces_to_class_map[class_set] - dbus_to_python_member_map = ( - python_class._dbus_meta.dbus_member_to_python_attr + dbus_to_python_member_map: Dict[str, Dict[str, str]] = ( + { + interface_name: meta.dbus_member_to_python_attr + for interface_name, meta in + python_class._dbus_iter_interfaces_meta() + } ) except KeyError: if on_unknown_interface == 'error': @@ -142,10 +147,13 @@ def parse_interfaces_added( dbus_to_python_member_map = {} python_properties: Dict[str, Any] = {} - for _, properties in properties_data.items(): + for interface_name, properties in properties_data.items(): + interface_member_map = dbus_to_python_member_map.get( + interface_name, {}, + ) python_properties.update( _parse_properties_vardict( - dbus_to_python_member_map, + interface_member_map, properties, on_unknown_member, ) @@ -195,4 +203,5 @@ def parse_interfaces_removed( __all__ = ( 'parse_properties_changed', 'parse_interfaces_added', + 'parse_interfaces_removed', ) diff --git a/test/test_low_level_errors.py b/test/test_low_level_errors.py index d8631ea..167a434 100644 --- a/test/test_low_level_errors.py +++ b/test/test_low_level_errors.py @@ -49,7 +49,7 @@ class IndependentError(Exception): class InterfaceWithErrors( DbusInterfaceCommonAsync, - interface_name='org.example.test', + interface_name='org.example.errors', ): @dbus_property_async('s') def indep_err_getter(self) -> str: diff --git a/test/test_object_manager.py b/test/test_object_manager.py index 9772baa..24be0f5 100644 --- a/test/test_object_manager.py +++ b/test/test_object_manager.py @@ -39,7 +39,7 @@ class ObjectManagerTestInterface( DbusObjectManagerInterfaceAsync, - interface_name='org.test.test', + interface_name='org.test.objectmanager', ): @dbus_method_async( result_signature='s', diff --git a/test/test_sdbus_async.py b/test/test_sdbus_async.py index 523ea14..ca9b762 100644 --- a/test/test_sdbus_async.py +++ b/test/test_sdbus_async.py @@ -91,9 +91,13 @@ async def test_request_name(self) -> None: await self.bus.request_name_async("org.example.test", 0) -class TestInterface(DbusInterfaceCommonAsync, - interface_name='org.test.test', - ): +TEST_INTERFACE_NAME = "org.test.test" + + +class TestInterface( + DbusInterfaceCommonAsync, + interface_name=TEST_INTERFACE_NAME, +): def __init__(self) -> None: super().__init__() @@ -387,19 +391,26 @@ def test_property_setter(self, var: str) -> None: self.assertEqual('12345', await test_subclass.test_property) with self.subTest('Test dbus to python mapping'): + dbus_elements_map = ( + { + interface_name: meta.dbus_member_to_python_attr + for interface_name, meta in + TestInterface._dbus_iter_interfaces_meta() + } + ) self.assertIn( "TestInt", - test_object._dbus_meta.dbus_member_to_python_attr, + dbus_elements_map[TEST_INTERFACE_NAME], ) self.assertIn( "TestInt", - test_subclass._dbus_meta.dbus_member_to_python_attr, + dbus_elements_map[TEST_INTERFACE_NAME], ) self.assertIn( "TestProperty", - test_subclass._dbus_meta.dbus_member_to_python_attr, + dbus_elements_map[TEST_INTERFACE_NAME], ) with self.subTest('Tripple subclass'): @@ -755,7 +766,7 @@ async def test_properties_get_all_dict(self) -> None: test_object, test_object_connection = initialize_object() dbus_dict = await test_object_connection._properties_get_all( - 'org.test.test') + TEST_INTERFACE_NAME) self.assertEqual( await test_object.test_property, diff --git a/test/test_sdbus_async_bad_class.py b/test/test_sdbus_async_bad_class.py index a8b345a..92e2ee0 100644 --- a/test/test_sdbus_async_bad_class.py +++ b/test/test_sdbus_async_bad_class.py @@ -19,6 +19,7 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA from __future__ import annotations +from gc import collect from unittest import TestCase from unittest import main as unittest_main @@ -40,7 +41,7 @@ class TestInterface( DbusInterfaceCommonAsync, - interface_name="org.example.test", + interface_name="org.example.good", ): @dbus_method_async(result_signature="i") async def test_int(self) -> int: @@ -138,7 +139,7 @@ def test_property_flags(self) -> None: skip_if_no_asserts() class InvalidPropertiesFlags( - DbusInterfaceCommonAsync, interface_name="org.test.test" + DbusInterfaceCommonAsync, interface_name="org.test.invalidprop" ): @dbus_property_async( "s", @@ -150,7 +151,7 @@ def test_constant(self) -> str: with self.subTest("Valid properties flags"): class ValidPropertiesFlags( - DbusInterfaceCommonAsync, interface_name="org.test.test" + DbusInterfaceCommonAsync, interface_name="org.test.validprop" ): @dbus_property_async( "s", @@ -160,13 +161,13 @@ def test_constant(self) -> str: return "a" def test_bad_subclass(self) -> None: - with self.assertRaises(TypeError): + with self.assertRaises(ValueError): class TestInheritence(TestInterface): async def test_int(self) -> int: return 2 - with self.assertRaises(TypeError): + with self.assertRaises(ValueError): class TestInheritence2(TestInterface): @dbus_method_async_override() @@ -189,6 +190,61 @@ class NoInterfaceName(TestInterface): async def example(self) -> None: ... + def test_shared_parent_class(self) -> None: + class One(TestInterface): + ... + + class Two(TestInterface): + ... + + class Shared(One, Two): + ... + + def test_combined_collision(self) -> None: + + class One( + DbusInterfaceCommonAsync, + interface_name="org.example.foo", + ): + @dbus_method_async() + async def example(self) -> None: + ... + + class Two( + DbusInterfaceCommonAsync, + interface_name="org.example.bar", + ): + @dbus_method_async() + async def example(self) -> None: + ... + + with self.assertRaisesRegex(ValueError, "collision"): + class Combined(One, Two): + ... + + def test_class_cleanup(self) -> None: + class One( + DbusInterfaceCommonAsync, + interface_name="org.example.foo1", + ): + ... + + with self.assertRaises(ValueError): + class Two( + DbusInterfaceCommonAsync, + interface_name="org.example.foo1", + ): + ... + + del One + collect() # Let weak refs be processed + + class After( + DbusInterfaceCommonAsync, + interface_name="org.example.foo1", + ): + ... + if __name__ == "__main__": unittest_main() diff --git a/test/test_sdbus_async_introspection.py b/test/test_sdbus_async_introspection.py index 20625be..2ad8e46 100644 --- a/test/test_sdbus_async_introspection.py +++ b/test/test_sdbus_async_introspection.py @@ -52,7 +52,7 @@ async def asyncSetUp(self) -> None: async def test_method_arg_names_none(self) -> None: class TestInterface( DbusInterfaceCommonAsync, - interface_name="org.test.test", + interface_name="org.test.intro1", ): @dbus_method_async( input_signature="ss", @@ -75,7 +75,7 @@ async def login( async def test_method_arg_names_result_names_only(self) -> None: class TestInterface( DbusInterfaceCommonAsync, - interface_name="org.test.test", + interface_name="org.test.intro2", ): @dbus_method_async( input_signature="ss", @@ -99,7 +99,7 @@ async def login( async def test_method_arg_names_full(self) -> None: class TestInterface( DbusInterfaceCommonAsync, - interface_name="org.test.test", + interface_name="org.test.intro3", ): @dbus_method_async( input_signature="ss", @@ -124,7 +124,7 @@ async def login( async def test_method_arg_names_no_return_args(self) -> None: class TestInterface( DbusInterfaceCommonAsync, - interface_name="org.test.test", + interface_name="org.test.intro4", ): @dbus_method_async( input_signature="ss", From 38e5013e131bd2b085f9a0fd13c387b5cbeab181 Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sun, 10 Mar 2024 17:58:14 +0500 Subject: [PATCH 086/188] Do not request properties of internal interfaces in get_all methods Those interfaces do not have properties and calling them only creates more round trips. --- src/sdbus/dbus_common_elements.py | 7 ++++++- src/sdbus/dbus_proxy_async_interface_base.py | 2 +- src/sdbus/dbus_proxy_async_interfaces.py | 3 +++ src/sdbus/dbus_proxy_sync_interface_base.py | 2 +- src/sdbus/dbus_proxy_sync_interfaces.py | 3 +++ 5 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/sdbus/dbus_common_elements.py b/src/sdbus/dbus_common_elements.py index 1f66d8b..f1a3ccb 100644 --- a/src/sdbus/dbus_common_elements.py +++ b/src/sdbus/dbus_common_elements.py @@ -344,7 +344,12 @@ def __init__(self) -> None: class DbusClassMeta: - def __init__(self, interface_name: str) -> None: + def __init__( + self, + interface_name: str, + serving_enabled: bool, + ) -> None: self.interface_name = interface_name + self.serving_enabled = serving_enabled self.dbus_member_to_python_attr: Dict[str, str] = {} self.python_attr_to_dbus_member: Dict[str, str] = {} diff --git a/src/sdbus/dbus_proxy_async_interface_base.py b/src/sdbus/dbus_proxy_async_interface_base.py index 3f48723..6afaccd 100644 --- a/src/sdbus/dbus_proxy_async_interface_base.py +++ b/src/sdbus/dbus_proxy_async_interface_base.py @@ -278,7 +278,7 @@ def __new__(cls, name: str, ) if interface_name is not None: - dbus_class_meta = DbusClassMeta(interface_name) + dbus_class_meta = DbusClassMeta(interface_name, serving_enabled) DBUS_CLASS_TO_META[new_cls] = dbus_class_meta DBUS_INTERFACE_NAME_TO_CLASS[interface_name] = new_cls diff --git a/src/sdbus/dbus_proxy_async_interfaces.py b/src/sdbus/dbus_proxy_async_interfaces.py index bb09810..b833d14 100644 --- a/src/sdbus/dbus_proxy_async_interfaces.py +++ b/src/sdbus/dbus_proxy_async_interfaces.py @@ -89,6 +89,9 @@ async def properties_get_all_dict( properties: Dict[str, Any] = {} for interface_name, meta in self._dbus_iter_interfaces_meta(): + if not meta.serving_enabled: + continue + dbus_properties_data = await self._properties_get_all( interface_name) diff --git a/src/sdbus/dbus_proxy_sync_interface_base.py b/src/sdbus/dbus_proxy_sync_interface_base.py index 777e765..fce36ba 100644 --- a/src/sdbus/dbus_proxy_sync_interface_base.py +++ b/src/sdbus/dbus_proxy_sync_interface_base.py @@ -154,7 +154,7 @@ def __new__(cls, name: str, ) if interface_name is not None: - dbus_class_meta = DbusClassMeta(interface_name) + dbus_class_meta = DbusClassMeta(interface_name, serving_enabled) DBUS_CLASS_TO_META[new_cls] = dbus_class_meta DBUS_INTERFACE_NAME_TO_CLASS[interface_name] = new_cls diff --git a/src/sdbus/dbus_proxy_sync_interfaces.py b/src/sdbus/dbus_proxy_sync_interfaces.py index f8f3dbb..066891c 100644 --- a/src/sdbus/dbus_proxy_sync_interfaces.py +++ b/src/sdbus/dbus_proxy_sync_interfaces.py @@ -71,6 +71,9 @@ def properties_get_all_dict( properties: Dict[str, Any] = {} for interface_name, meta in self._dbus_iter_interfaces_meta(): + if not meta.serving_enabled: + continue + dbus_properties_data = self._properties_get_all(interface_name) for member_name, variant in dbus_properties_data.items(): try: From 66e303ff0d12fde2e7b8eaa0343ba3af002d4fc4 Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sun, 10 Mar 2024 18:14:11 +0500 Subject: [PATCH 087/188] Run all linters even if one fails Often it is useful to run mypy even if there are formatting errors. --- tools/run_py_linters.py | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/tools/run_py_linters.py b/tools/run_py_linters.py index 30af375..adff96e 100755 --- a/tools/run_py_linters.py +++ b/tools/run_py_linters.py @@ -22,7 +22,7 @@ from argparse import ArgumentParser from os import environ from pathlib import Path -from subprocess import run +from subprocess import SubprocessError, run from typing import List source_root = Path(environ['MESON_SOURCE_ROOT']) @@ -61,7 +61,7 @@ def run_mypy() -> None: ) -def linter_main() -> None: +def run_flake8() -> None: run( args=( 'flake8', @@ -70,7 +70,22 @@ def linter_main() -> None: check=True, ) - run_mypy() + +def linter_main() -> None: + is_success = True + + try: + run_flake8() + except SubprocessError: + is_success = False + + try: + run_mypy() + except SubprocessError: + is_success = False + + if not is_success: + raise SystemExit(1) def get_all_python_files() -> List[Path]: From a8f7658b9ba0889f84907fba756a8ca904451e73 Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sun, 10 Mar 2024 18:34:37 +0500 Subject: [PATCH 088/188] Use _private names for metaclasses helper functions --- src/sdbus/dbus_proxy_async_interface_base.py | 24 ++++++++++---------- src/sdbus/dbus_proxy_sync_interface_base.py | 12 +++++----- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/sdbus/dbus_proxy_async_interface_base.py b/src/sdbus/dbus_proxy_async_interface_base.py index 6afaccd..0649291 100644 --- a/src/sdbus/dbus_proxy_async_interface_base.py +++ b/src/sdbus/dbus_proxy_async_interface_base.py @@ -77,7 +77,7 @@ class DbusInterfaceMetaAsync(DbusInterfaceMetaCommon): @staticmethod - def process_dbus_method_override( + def _process_dbus_method_override( override_attr_name: str, override: DbusMethodOverride, mro_dbus_elements: Dict[str, DbusSomethingAsync], @@ -101,7 +101,7 @@ def process_dbus_method_override( return new_method @staticmethod - def process_dbus_property_override( + def _process_dbus_property_override( override_attr_name: str, override: DbusPropertyOverride, mro_dbus_elements: Dict[str, DbusSomethingAsync], @@ -132,7 +132,7 @@ def process_dbus_property_override( return new_property @classmethod - def check_collisions( + def _check_collisions( cls, new_class_name: str, namespace: Dict[str, Any], @@ -144,14 +144,14 @@ def check_collisions( for attr_name, attr in namespace.items(): if isinstance(attr, DbusMethodOverride): - new_overrides[attr_name] = cls.process_dbus_method_override( + new_overrides[attr_name] = cls._process_dbus_method_override( attr_name, attr, mro_dbus_elements, ) possible_collisions.remove(attr_name) elif isinstance(attr, DbusPropertyOverride): - new_overrides[attr_name] = cls.process_dbus_property_override( + new_overrides[attr_name] = cls._process_dbus_property_override( attr_name, attr, mro_dbus_elements, @@ -169,7 +169,7 @@ def check_collisions( namespace.update(new_overrides) @staticmethod - def extract_dbus_elements( + def _extract_dbus_elements( dbus_class: type, dbus_meta: DbusClassMeta, ) -> Dict[str, DbusSomethingAsync]: @@ -188,7 +188,7 @@ def extract_dbus_elements( return dbus_elements_map @classmethod - def map_mro_dbus_elements( + def _map_mro_dbus_elements( cls, new_class_name: str, base_classes: Iterable[type], @@ -201,7 +201,7 @@ def map_mro_dbus_elements( if dbus_meta is None: continue - base_dbus_elements = cls.extract_dbus_elements(c, dbus_meta) + base_dbus_elements = cls._extract_dbus_elements(c, dbus_meta) possible_collisions.update( base_dbus_elements.keys() & all_python_dbus_map.keys() @@ -220,7 +220,7 @@ def map_mro_dbus_elements( return all_python_dbus_map @staticmethod - def map_dbus_elements( + def _map_dbus_elements( attr_name: str, attr: Any, meta: DbusClassMeta, @@ -266,10 +266,10 @@ def __new__(cls, name: str, all_mro_bases: Set[Type[Any]] = set( chain.from_iterable((c.__mro__ for c in bases)) ) - reserved_dbus_map = cls.map_mro_dbus_elements( + reserved_dbus_map = cls._map_mro_dbus_elements( name, all_mro_bases, ) - cls.check_collisions(name, namespace, reserved_dbus_map) + cls._check_collisions(name, namespace, reserved_dbus_map) new_cls = super().__new__( cls, name, bases, namespace, @@ -283,7 +283,7 @@ def __new__(cls, name: str, DBUS_INTERFACE_NAME_TO_CLASS[interface_name] = new_cls for attr_name, attr in namespace.items(): - cls.map_dbus_elements( + cls._map_dbus_elements( attr_name, attr, dbus_class_meta, diff --git a/src/sdbus/dbus_proxy_sync_interface_base.py b/src/sdbus/dbus_proxy_sync_interface_base.py index fce36ba..e749fb8 100644 --- a/src/sdbus/dbus_proxy_sync_interface_base.py +++ b/src/sdbus/dbus_proxy_sync_interface_base.py @@ -57,7 +57,7 @@ class DbusInterfaceMetaSync(DbusInterfaceMetaCommon): @staticmethod - def check_collisions( + def _check_collisions( new_class_name: str, attr_names: Set[str], reserved_attr_names: Set[str], @@ -71,7 +71,7 @@ def check_collisions( ) @staticmethod - def collect_dbus_to_python_attr_names( + def _collect_dbus_to_python_attr_names( new_class_name: str, base_classes: Iterable[type], ) -> Set[str]: @@ -104,7 +104,7 @@ def collect_dbus_to_python_attr_names( return all_python_dbus_attrs @staticmethod - def map_dbus_elements( + def _map_dbus_elements( attr_name: str, attr: Any, meta: DbusClassMeta, @@ -142,10 +142,10 @@ def __new__(cls, name: str, all_mro_bases: Set[Type[Any]] = set( chain.from_iterable((c.__mro__ for c in bases)) ) - reserved_attr_names = cls.collect_dbus_to_python_attr_names( + reserved_attr_names = cls._collect_dbus_to_python_attr_names( name, all_mro_bases, ) - cls.check_collisions(name, set(namespace.keys()), reserved_attr_names) + cls._check_collisions(name, set(namespace.keys()), reserved_attr_names) new_cls = super().__new__( cls, name, bases, namespace, @@ -159,7 +159,7 @@ def __new__(cls, name: str, DBUS_INTERFACE_NAME_TO_CLASS[interface_name] = new_cls for attr_name, attr in namespace.items(): - cls.map_dbus_elements(attr_name, attr, dbus_class_meta) + cls._map_dbus_elements(attr_name, attr, dbus_class_meta) return new_cls From 04d14b0233b344638b0e1cb3f649d961fa2e9b83 Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sun, 10 Mar 2024 19:06:24 +0500 Subject: [PATCH 089/188] test: Use more assertDbusSignalEmits instead of sleep --- test/test_sdbus_async.py | 40 +++++++++------------------------------- 1 file changed, 9 insertions(+), 31 deletions(-) diff --git a/test/test_sdbus_async.py b/test/test_sdbus_async.py index ca9b762..8d745bd 100644 --- a/test/test_sdbus_async.py +++ b/test/test_sdbus_async.py @@ -21,7 +21,7 @@ from asyncio import Event, get_running_loop, sleep, wait_for from asyncio.subprocess import create_subprocess_exec -from typing import TYPE_CHECKING, cast +from typing import TYPE_CHECKING from unittest import SkipTest from sdbus.exceptions import ( @@ -798,23 +798,12 @@ async def test_properties_changed(self) -> None: test_str = 'should_be_emited' - properties_changed_dbus_aiter = ( - test_object_connection.properties_changed.__aiter__() - ) - - async def set_property() -> None: - await sleep(0.1) + async with self.assertDbusSignalEmits( + test_object_connection.properties_changed + ) as properties_changed_catch: await test_object_connection.test_property.set_async(test_str) - get_running_loop().create_task(set_property()) - - properties_changed_data = cast( - DBUS_PROPERTIES_CHANGED_TYPING, - await wait_for( - properties_changed_dbus_aiter.__anext__(), - timeout=1 - ), - ) + properties_changed_data = properties_changed_catch.output[0] parsed_dict_from_class = parse_properties_changed( TestInterface, properties_changed_data) @@ -851,23 +840,12 @@ async def test_property_private_setter(self) -> None: await test_object_connection.test_property_private.set_async( new_value) - properties_changed_dbus_aiter = ( - test_object_connection.properties_changed.__aiter__() - ) - - async def set_property() -> None: - await sleep(0.1) + async with self.assertDbusSignalEmits( + test_object_connection.properties_changed + ) as properties_changed_catch: await test_object.test_property_private.set_async(new_value) - get_running_loop().create_task(set_property()) - - changed_properties = cast( - DBUS_PROPERTIES_CHANGED_TYPING, - await wait_for( - properties_changed_dbus_aiter.__anext__(), - timeout=1, - ), - ) + changed_properties = properties_changed_catch.output[0] self.assertEqual( await test_object_connection.test_property_private, From 87a56f86ee3aa6c0fb9c897a4f9f0ace5dc939ca Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sun, 10 Mar 2024 21:58:31 +0500 Subject: [PATCH 090/188] Add sdbus.utils.parse_get_managed_objects Parses data from ObjectsManager's `get_managed_objects` calls. It is similar to existing `parse_interfaces_added` function. Also reduce code duplication in sdbus.utils using extra functions. --- docs/utils.rst | 19 ++++ src/sdbus/utils.py | 219 +++++++++++++++++++++++------------- test/test_object_manager.py | 100 +++++++++++++++- 3 files changed, 259 insertions(+), 79 deletions(-) diff --git a/docs/utils.rst b/docs/utils.rst index 39ae835..abdc922 100644 --- a/docs/utils.rst +++ b/docs/utils.rst @@ -59,3 +59,22 @@ Parsing utilities of interface class. :rtype: Tuple[str, Optional[Type[DbusInterfaceBaseAsync]]] :returns: Path of removed object and object's class (or ``None``). + +.. py:function:: parse_get_managed_objects(interfaces, managed_objects_data, on_unknown_interface='error', on_unknown_member='error') + + Parse data from :py:meth:`get_managed_objects ` call. + + Takes an iterable of D-Bus interface classes (or a single class) and the method returned data. + Returns a dictionary where keys a paths of the managed objects and value is a tuple of class of the object + and dictionary of its python named properties and their values. + + :param Iterable[DbusInterfaceBaseAsync] interfaces: Possible interfaces of the managed objects. + Can accept classes with multiple interfaces defined. + :param Dict interfaces_added_data: Data returned by ``get_managed_objects`` call. + :param str on_unknown_member: If an unknown D-Bus interface was encountered + either raise an ``"error"`` (default) or return ``"none"`` instead + of interface class. + :rtype: Dict[str, Tuple[Optional[Type[DbusInterfaceBaseAsync], Dict[str, Any]]]] + :returns: Dictionary where keys are paths and values are tuples of managed objects classes and their properties data. + + *New in version 0.12.0.* diff --git a/src/sdbus/utils.py b/src/sdbus/utils.py index f2be439..7c666a3 100644 --- a/src/sdbus/utils.py +++ b/src/sdbus/utils.py @@ -44,11 +44,27 @@ from .dbus_proxy_async_interfaces import DBUS_PROPERTIES_CHANGED_TYPING + InterfacesInputElements = Union[ + DbusInterfaceBaseAsync, + Type[DbusInterfaceBaseAsync], + ] + InterfacesInput = Union[ + InterfacesInputElements, + Iterable[InterfacesInputElements], + ] + InterfacesToClassMap = Dict[FrozenSet[str], Type[DbusInterfaceBaseAsync]] + OnUnknownMember = Literal['error', 'ignore', 'reuse'] + OnUnknownInterface = Literal['error', 'none'] + ParseGetManaged = Dict[ + str, + Tuple[Optional[Type[DbusInterfaceBaseAsync]], Dict[str, Any]], + ] + def parse_properties_changed( - interface: Union[DbusInterfaceBaseAsync, Type[DbusInterfaceBaseAsync]], + interface: InterfacesInputElements, properties_changed_data: DBUS_PROPERTIES_CHANGED_TYPING, - on_unknown_member: Literal['error', 'ignore', 'reuse'] = 'error', + on_unknown_member: OnUnknownMember = 'error', ) -> Dict[str, Any]: interface_name, changed_properties, invalidated_properties = ( properties_changed_data @@ -75,17 +91,16 @@ def parse_properties_changed( def _create_interfaces_map( - interfaces_iter: Iterable[ - Union[ - DbusInterfaceBaseAsync, - Type[DbusInterfaceBaseAsync], - ] - ] -) -> Dict[FrozenSet[str], Type[DbusInterfaceBaseAsync]]: - interfaces_to_class_map: Dict[ - FrozenSet[str], - Type[DbusInterfaceBaseAsync], - ] = {} + interfaces: InterfacesInput, +) -> InterfacesToClassMap: + + if isinstance(interfaces, + (DbusInterfaceBaseAsync, type)): + interfaces_iter = iter((interfaces, )) + else: + interfaces_iter = iter(interfaces) + + interfaces_to_class_map: InterfacesToClassMap = {} for interface in interfaces_iter: interface_names_set = frozenset( @@ -101,51 +116,74 @@ def _create_interfaces_map( return interfaces_to_class_map +def _get_class_from_interfaces( + interfaces_to_class_map: InterfacesToClassMap, + interface_names_iter: Iterable[str], + raise_key_error: bool, +) -> Optional[Type[DbusInterfaceBaseAsync]]: + class_set = frozenset(interface_names_iter) - SKIP_INTERFACES + try: + return interfaces_to_class_map[class_set] + except KeyError: + if raise_key_error: + raise + + return None + + +def _get_member_map_from_class( + python_class: Optional[Type[DbusInterfaceBaseAsync]], +) -> Dict[str, Dict[str, str]]: + if python_class is None: + return {} + else: + return { + interface_name: meta.dbus_member_to_python_attr + for interface_name, meta in + python_class._dbus_iter_interfaces_meta() + } + + +def _translate_and_merge_members( + properties_data: Dict[str, Dict[str, Any]], + dbus_to_python_map: Dict[str, Dict[str, str]], + on_unknown_member: OnUnknownMember, +) -> Dict[str, Any]: + python_properties: Dict[str, Any] = {} + for interface_name, properties in properties_data.items(): + interface_member_map = dbus_to_python_map.get( + interface_name, {}, + ) + python_properties.update( + _parse_properties_vardict( + interface_member_map, + properties, + on_unknown_member, + ) + ) + + return python_properties + + def parse_interfaces_added( - interfaces: Union[ - Union[ - DbusInterfaceBaseAsync, - Type[DbusInterfaceBaseAsync], - ], - Iterable[ - Union[ - DbusInterfaceBaseAsync, - Type[DbusInterfaceBaseAsync], - ], - ], - ], + interfaces: InterfacesInput, interfaces_added_data: Tuple[str, Dict[str, Dict[str, Any]]], - on_unknown_interface: Literal['error', 'none'] = 'error', - on_unknown_member: Literal['error', 'ignore', 'reuse'] = 'error', + on_unknown_interface: OnUnknownInterface = 'error', + on_unknown_member: OnUnknownMember = 'error', ) -> Tuple[str, Optional[Type[DbusInterfaceBaseAsync]], Dict[str, Any]]: - if isinstance(interfaces, - (DbusInterfaceBaseAsync, type)): - interfaces_iter = iter((interfaces, )) - else: - interfaces_iter = iter(interfaces) - - interfaces_to_class_map = _create_interfaces_map(interfaces_iter) + interfaces_to_class_map = _create_interfaces_map(interfaces) path, properties_data = interfaces_added_data - class_set = frozenset(properties_data.keys()) - SKIP_INTERFACES - try: - python_class = interfaces_to_class_map[class_set] - dbus_to_python_member_map: Dict[str, Dict[str, str]] = ( - { - interface_name: meta.dbus_member_to_python_attr - for interface_name, meta in - python_class._dbus_iter_interfaces_meta() - } + python_class = ( + _get_class_from_interfaces( + interfaces_to_class_map, + properties_data.keys(), + on_unknown_interface == "error", ) - except KeyError: - if on_unknown_interface == 'error': - raise - - python_class = None - dbus_to_python_member_map = {} - + ) + dbus_to_python_member_map = _get_member_map_from_class(python_class) python_properties: Dict[str, Any] = {} for interface_name, properties in properties_data.items(): interface_member_map = dbus_to_python_member_map.get( @@ -159,49 +197,74 @@ def parse_interfaces_added( ) ) - return path, python_class, python_properties + return ( + path, + python_class, + _translate_and_merge_members( + properties_data, + dbus_to_python_member_map, + on_unknown_member, + ), + ) def parse_interfaces_removed( - interfaces: Union[ - Union[ - DbusInterfaceBaseAsync, - Type[DbusInterfaceBaseAsync], - ], - Iterable[ - Union[ - DbusInterfaceBaseAsync, - Type[DbusInterfaceBaseAsync], - ], - ], - ], + interfaces: InterfacesInput, interfaces_removed_data: Tuple[str, List[str]], - on_unknown_interface: Literal['error', 'none'] = 'error', + on_unknown_interface: OnUnknownInterface = 'error', ) -> Tuple[str, Optional[Type[DbusInterfaceBaseAsync]]]: - if isinstance(interfaces, - (DbusInterfaceBaseAsync, type)): - interfaces_iter = iter((interfaces, )) - else: - interfaces_iter = iter(interfaces) - interfaces_to_class_map = _create_interfaces_map(interfaces_iter) + interfaces_to_class_map = _create_interfaces_map(interfaces) path, interfaces_removed = interfaces_removed_data - class_set = frozenset(interfaces_removed) - SKIP_INTERFACES - try: - python_class = interfaces_to_class_map[class_set] - except KeyError: - if on_unknown_interface == 'error': - raise - - python_class = None + python_class = ( + _get_class_from_interfaces( + interfaces_to_class_map, + interfaces_removed, + on_unknown_interface == "error", + ) + ) return path, python_class +def parse_get_managed_objects( + interfaces: InterfacesInput, + managed_objects_data: Dict[str, Dict[str, Dict[str, Any]]], + on_unknown_interface: OnUnknownInterface = 'error', + on_unknown_member: OnUnknownMember = 'error', +) -> ParseGetManaged: + + interfaces_to_class_map = _create_interfaces_map(interfaces) + + managed_objects_map: ParseGetManaged = {} + + for path, properties_data in managed_objects_data.items(): + python_class = ( + _get_class_from_interfaces( + interfaces_to_class_map, + properties_data.keys(), + on_unknown_interface == "error", + ) + ) + dbus_to_python_member_map = _get_member_map_from_class(python_class) + + managed_objects_map[path] = ( + python_class, + _translate_and_merge_members( + properties_data, + dbus_to_python_member_map, + on_unknown_member, + ), + ) + + return managed_objects_map + + __all__ = ( 'parse_properties_changed', 'parse_interfaces_added', 'parse_interfaces_removed', + 'parse_get_managed_objects', ) diff --git a/test/test_object_manager.py b/test/test_object_manager.py index 24be0f5..39e7828 100644 --- a/test/test_object_manager.py +++ b/test/test_object_manager.py @@ -24,7 +24,11 @@ from typing import Any, Dict, List, Tuple from sdbus.unittest import IsolatedDbusTestCase -from sdbus.utils import parse_interfaces_added, parse_interfaces_removed +from sdbus.utils import ( + parse_get_managed_objects, + parse_interfaces_added, + parse_interfaces_removed, +) from sdbus import ( DbusInterfaceCommonAsync, @@ -253,6 +257,100 @@ async def catch_interfaces_removed() -> Tuple[str, List[str]]: self.assertIn('TestStr', python_properties) self.assertIn('TestInt', python_properties) + get_managed_data = ( + await object_manager_connection.get_managed_objects() + ) + with self.subTest('Parse get managed objects class'): + managed_dict = ( + parse_get_managed_objects( + ManagedTwoInterface, + get_managed_data, + ) + ) + + self.assertIn(MANAGED_PATH, managed_dict) + managed_class, managed_properties = ( + managed_dict[MANAGED_PATH] + ) + self.assertEqual(managed_class, ManagedTwoInterface) + self.assertIn('test_str', managed_properties) + self.assertIn('test_int', managed_properties) + + with self.subTest('Parse get managed objects object'): + managed_dict = ( + parse_get_managed_objects( + managed_object, + get_managed_data, + ) + ) + + self.assertIn(MANAGED_PATH, managed_dict) + managed_class, managed_properties = ( + managed_dict[MANAGED_PATH] + ) + self.assertEqual(managed_class, ManagedTwoInterface) + self.assertIn('test_str', managed_properties) + self.assertIn('test_int', managed_properties) + + with self.subTest('Parse get managed objects iterable'): + managed_dict = ( + parse_get_managed_objects( + (ManagedInterface, ManagedTwoInterface), + get_managed_data, + ) + ) + + self.assertIn(MANAGED_PATH, managed_dict) + managed_class, managed_properties = ( + managed_dict[MANAGED_PATH] + ) + self.assertEqual(managed_class, ManagedTwoInterface) + self.assertIn('test_str', managed_properties) + self.assertIn('test_int', managed_properties) + + with self.subTest('Parse get managed objects unknown'): + with self.assertRaises(KeyError): + managed_dict = ( + parse_get_managed_objects( + ManagedInterface, + get_managed_data, + ) + ) + + with self.assertRaises(KeyError): + managed_dict = ( + parse_get_managed_objects( + ManagedInterface, + get_managed_data, + on_unknown_interface='none', + ) + ) + + managed_dict = ( + parse_get_managed_objects( + ManagedInterface, + get_managed_data, + on_unknown_interface='none', + on_unknown_member='reuse', + ) + ) + path, python_class, python_properties = ( + parse_interfaces_added( + ManagedInterface, + caught_added, + on_unknown_interface='none', + on_unknown_member='reuse', + ) + ) + + self.assertIn(MANAGED_PATH, managed_dict) + managed_class, managed_properties = ( + managed_dict[MANAGED_PATH] + ) + self.assertIsNone(managed_class) + self.assertIn('TestStr', managed_properties) + self.assertIn('TestInt', managed_properties) + object_manager.remove_managed_object(managed_object) interfaces_removed_data = await wait_for( From 6f7f035aabae101879b53b53b02c4fe69d50c9b2 Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sat, 16 Mar 2024 15:44:43 +0500 Subject: [PATCH 091/188] test: Use assertDbusSignalEmits for object manager tests Less race conditions as assertDbusSignalEmits ensures that all match rules are setup before the inner loop in entered. --- test/test_object_manager.py | 76 ++++++++++--------------------------- 1 file changed, 20 insertions(+), 56 deletions(-) diff --git a/test/test_object_manager.py b/test/test_object_manager.py index 39e7828..2d0e835 100644 --- a/test/test_object_manager.py +++ b/test/test_object_manager.py @@ -17,12 +17,8 @@ # 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 asyncio import get_running_loop, sleep, wait_for -from typing import Any, Dict, List, Tuple - from sdbus.unittest import IsolatedDbusTestCase from sdbus.utils import ( parse_get_managed_objects, @@ -73,7 +69,6 @@ def test_int(self) -> int: class TestObjectManager(IsolatedDbusTestCase): async def test_object_manager(self) -> None: - loop = get_running_loop() await self.bus.request_name_async(CONNECTION_NAME, 0) object_manager = ObjectManagerTestInterface() @@ -86,31 +81,14 @@ async def test_object_manager(self) -> None: await object_manager_connection.get_hello_world(), HELLO_WORLD) - async def catch_interfaces_added() -> Tuple[str, - Dict[str, - Dict[str, Any]]]: - async for x in object_manager_connection.interfaces_added: - return x - - raise RuntimeError - - catch_added_task = loop.create_task(catch_interfaces_added()) - - async def catch_interfaces_removed() -> Tuple[str, List[str]]: - async for x in object_manager_connection.interfaces_removed: - return x - - raise RuntimeError - - catch_removed_task = loop.create_task(catch_interfaces_removed()) - - await sleep(0) - managed_object = ManagedInterface() - object_manager.export_with_manager(MANAGED_PATH, managed_object) + async with self.assertDbusSignalEmits( + object_manager_connection.interfaces_added + ) as added_interfaces_catch: + object_manager.export_with_manager(MANAGED_PATH, managed_object) - caught_added = await wait_for(catch_added_task, timeout=0.5) + caught_added = added_interfaces_catch.output[0] added_path, added_attributes = caught_added @@ -126,10 +104,12 @@ async def catch_interfaces_removed() -> Tuple[str, List[str]]: with self.subTest("Test interfaces added parser"): parse_interfaces_added(ManagedInterface, caught_added) - object_manager.remove_managed_object(managed_object) + async with self.assertDbusSignalEmits( + object_manager_connection.interfaces_removed + ) as removed_interfaces_catch: + object_manager.remove_managed_object(managed_object) - path_removed, interfaces_removed = await wait_for( - catch_removed_task, timeout=1) + path_removed, interfaces_removed = removed_interfaces_catch.output[0] self.assertEqual(path_removed, MANAGED_PATH) @@ -159,7 +139,6 @@ class ManagedTwoInterface( def test_str(self) -> str: return 'test' - loop = get_running_loop() await self.bus.request_name_async(CONNECTION_NAME, 0) object_manager = DbusObjectManagerInterfaceAsync() @@ -168,31 +147,14 @@ def test_str(self) -> str: object_manager_connection = DbusObjectManagerInterfaceAsync.new_proxy( CONNECTION_NAME, OBJECT_MANAGER_PATH) - async def catch_interfaces_added() -> Tuple[str, - Dict[str, - Dict[str, Any]]]: - async for x in object_manager_connection.interfaces_added: - return x - - raise RuntimeError - - catch_added_task = loop.create_task(catch_interfaces_added()) - - async def catch_interfaces_removed() -> Tuple[str, List[str]]: - async for x in object_manager_connection.interfaces_removed: - return x - - raise RuntimeError - - catch_removed_task = loop.create_task(catch_interfaces_removed()) - - await sleep(0) - managed_object = ManagedTwoInterface() - object_manager.export_with_manager(MANAGED_PATH, managed_object) + async with self.assertDbusSignalEmits( + object_manager_connection.interfaces_added + ) as added_interfaces_catch: + object_manager.export_with_manager(MANAGED_PATH, managed_object) - caught_added = await wait_for(catch_added_task, timeout=0.5) + caught_added = added_interfaces_catch.output[0] with self.subTest('Parse added class'): path, python_class, python_properties = ( @@ -351,10 +313,12 @@ async def catch_interfaces_removed() -> Tuple[str, List[str]]: self.assertIn('TestStr', managed_properties) self.assertIn('TestInt', managed_properties) - object_manager.remove_managed_object(managed_object) + async with self.assertDbusSignalEmits( + object_manager_connection.interfaces_removed + ) as removed_interfaces_catch: + object_manager.remove_managed_object(managed_object) - interfaces_removed_data = await wait_for( - catch_removed_task, timeout=1) + interfaces_removed_data = removed_interfaces_catch.output[0] with self.subTest('Parse removed class'): path, python_class = ( From 331b6e91296490a280b3bc5510f263a12b24c39c Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sat, 16 Mar 2024 23:33:29 +0500 Subject: [PATCH 092/188] Added handle return to .export_to_dbus and .export_with_manager The handle can be used to remove the object from exports by either using it as a context manager or by calling the `.stop()` method of the handle. --- docs/asyncio_api.rst | 62 +++++++++ src/sdbus/__init__.py | 6 +- src/sdbus/dbus_proxy_async_interface_base.py | 41 +++++- src/sdbus/dbus_proxy_async_interfaces.py | 75 +---------- src/sdbus/dbus_proxy_async_object_manager.py | 130 +++++++++++++++++++ src/sdbus/sd_bus_internals.py | 1 + src/sdbus/sd_bus_internals_interface.c | 3 +- test/test_object_manager.py | 53 ++++++++ test/test_sdbus_async.py | 22 ++++ 9 files changed, 314 insertions(+), 79 deletions(-) create mode 100644 src/sdbus/dbus_proxy_async_object_manager.py diff --git a/docs/asyncio_api.rst b/docs/asyncio_api.rst index a1985ac..195756b 100644 --- a/docs/asyncio_api.rst +++ b/docs/asyncio_api.rst @@ -130,6 +130,35 @@ Classes Object will appear and become callable on D-Bus. + Returns a handle that can either be used as a context manager + to remove the object from D-Bus or ``.stop()`` method of the + handle can be called to remove object from D-Bus. + + .. code-block:: python + + with dbus_object.export_to_dbus("/"): + # dbus_object can be called from D-Bus inside this + # with block. + ... + + ... + + handle = dbus_object2.export_to_dbus("/") + # dbus_object2 can be called from D-Bus between these statements + handle.stop() + + ... + + dbus_object3.export_to_dbus("/") + # dbus_object3 can be called from D-Bus until all references are + # dropped. + del dbus_object3 + + If the handle is discarded the object will remain exported until + it gets deallocated. + + *Changed in version 0.12.0:* Added a handle return. + :param str object_path: Object path that it will be available at. @@ -137,6 +166,8 @@ Classes Optional D-Bus connection object. If not passed the default D-Bus will be used. + :return: Handle to control the export. + .. py:class:: DbusObjectManagerInterfaceAsync(interface_name) @@ -211,6 +242,36 @@ Classes ObjectManager will keep the reference to the object. + Returns a handle that can either be used as a context manager + to remove the object or ``.stop()`` method of the handle can be + called to remove object from D-Bus and drop reference to the object. + Signal will be emitted when the object is stopped via handle. + + .. code-block:: python + + manager = DbusObjectManagerInterfaceAsync() + manager.export_to_dbus('/object/manager') + + with manager.export_with_manager("/object/manager/example", dbus_object): + # dbus_object can be called from D-Bus inside this + # with block. + ... + + # Removed signal will be emitted once the with block exits + + ... + + handle = manager.export_with_manager("/object/manager/example", dbus_object2) + # dbus_object2 can be called from D-Bus between these statements + handle.stop() + # Removed signal will be emitted once the .stop() method is called + + If the handle is discarded the object will remain exported until + it gets removed from manager with :py:meth:`remove_managed_object` and + the object gets deallocated. + + *Changed in version 0.12.0:* Added a handle return. + :param str object_path: Object path that it will be available at. @@ -222,6 +283,7 @@ Classes If not passed the default D-Bus will be used. :raises RuntimeError: ObjectManager was not exported. + :return: Handle to control the export. .. py:method:: remove_managed_object(managed_object) diff --git a/src/sdbus/__init__.py b/src/sdbus/__init__.py index 28583f9..072f8c6 100644 --- a/src/sdbus/__init__.py +++ b/src/sdbus/__init__.py @@ -57,15 +57,13 @@ DbusUnknownObjectError, DbusUnknownPropertyError, ) -from .dbus_proxy_async_interfaces import ( - DbusInterfaceCommonAsync, - DbusObjectManagerInterfaceAsync, -) +from .dbus_proxy_async_interfaces import DbusInterfaceCommonAsync from .dbus_proxy_async_method import ( dbus_method_async, dbus_method_async_override, get_current_message, ) +from .dbus_proxy_async_object_manager import DbusObjectManagerInterfaceAsync from .dbus_proxy_async_property import ( dbus_property_async, dbus_property_async_override, diff --git a/src/sdbus/dbus_proxy_async_interface_base.py b/src/sdbus/dbus_proxy_async_interface_base.py index 0649291..2647897 100644 --- a/src/sdbus/dbus_proxy_async_interface_base.py +++ b/src/sdbus/dbus_proxy_async_interface_base.py @@ -62,7 +62,7 @@ ) from .dbus_common_elements import DbusBindedAsync - from .sd_bus_internals import SdBus + from .sd_bus_internals import SdBus, SdBusSlot Self = TypeVar('Self', bound="DbusInterfaceBaseAsync") DbusOverride = Union[DbusMethodOverride, DbusPropertyOverride] @@ -324,7 +324,7 @@ def export_to_dbus( self, object_path: str, bus: Optional[SdBus] = None, - ) -> None: + ) -> DbusExportHandle: local_object_meta = self._dbus if isinstance(local_object_meta, DbusRemoteObjectMeta): @@ -418,6 +418,8 @@ def export_to_dbus( interface_name) local_object_meta.activated_interfaces.append(new_interface) + return DbusExportHandle(local_object_meta) + def _connect( self, service_name: str, @@ -470,3 +472,38 @@ def new_proxy( new_object = cls.__new__(cls) new_object._proxify(service_name, object_path, bus) return new_object + + +class DbusExportHandle: + def __init__(self, local_meta: DbusLocalObjectMeta): + self._dbus_slots: List[SdBusSlot] = [ + i.slot + for i in local_meta.activated_interfaces + if i.slot is not None + ] + + async def __aenter__(self) -> DbusExportHandle: + return self + + def __enter__(self) -> DbusExportHandle: + return self + + def __exit__( + self, + exc_type: Any, + exc_value: Any, + traceback: Any, + ) -> None: + self.stop() + + async def __aexit__( + self, + exc_type: Any, + exc_value: Any, + traceback: Any, + ) -> None: + self.stop() + + def stop(self) -> None: + for slot in self._dbus_slots: + slot.close() diff --git a/src/sdbus/dbus_proxy_async_interfaces.py b/src/sdbus/dbus_proxy_async_interfaces.py index b833d14..ab8a1a6 100644 --- a/src/sdbus/dbus_proxy_async_interfaces.py +++ b/src/sdbus/dbus_proxy_async_interfaces.py @@ -1,6 +1,6 @@ # SPDX-License-Identifier: LGPL-2.1-or-later -# Copyright (C) 2020-2022 igo95862 +# Copyright (C) 2020-2024 igo95862 # This file is part of python-sdbus @@ -21,15 +21,13 @@ from typing import TYPE_CHECKING -from .dbus_common_funcs import _parse_properties_vardict, get_default_bus +from .dbus_common_funcs import _parse_properties_vardict from .dbus_proxy_async_interface_base import DbusInterfaceBaseAsync from .dbus_proxy_async_method import dbus_method_async from .dbus_proxy_async_signal import dbus_signal_async if TYPE_CHECKING: - from typing import Any, Dict, List, Literal, Optional, Tuple - - from .sd_bus_internals import SdBus, SdBusSlot + from typing import Any, Dict, List, Literal, Tuple DBUS_PROPERTIES_CHANGED_TYPING = ( Tuple[ @@ -110,70 +108,3 @@ class DbusInterfaceCommonAsync( DbusPeerInterfaceAsync, DbusPropertiesInterfaceAsync, DbusIntrospectableAsync): ... - - -class DbusObjectManagerInterfaceAsync( - DbusInterfaceCommonAsync, - interface_name='org.freedesktop.DBus.ObjectManager', - serving_enabled=False, -): - def __init__(self) -> None: - super().__init__() - self._object_manager_slot: Optional[SdBusSlot] = None - self._managed_object_to_path: Dict[DbusInterfaceBaseAsync, str] = {} - - @dbus_method_async(result_signature='a{oa{sa{sv}}}') - async def get_managed_objects( - self) -> Dict[str, Dict[str, Dict[str, Any]]]: - raise NotImplementedError - - @dbus_signal_async('oa{sa{sv}}') - def interfaces_added(self) -> Tuple[str, Dict[str, Dict[str, Any]]]: - raise NotImplementedError - - @dbus_signal_async('oao') - def interfaces_removed(self) -> Tuple[str, List[str]]: - raise NotImplementedError - - def export_to_dbus( - self, - object_path: str, - bus: Optional[SdBus] = None, - ) -> None: - if bus is None: - bus = get_default_bus() - - super().export_to_dbus( - object_path, - bus, - ) - slot = bus.add_object_manager(object_path) - self._object_manager_slot = slot - - def export_with_manager( - self, - object_path: str, - object_to_export: DbusInterfaceBaseAsync, - bus: Optional[SdBus] = None, - ) -> None: - if self._object_manager_slot is None: - raise RuntimeError('ObjectManager not intitialized') - - if bus is None: - bus = get_default_bus() - - object_to_export.export_to_dbus( - object_path, - bus, - ) - bus.emit_object_added(object_path) - self._managed_object_to_path[object_to_export] = object_path - - def remove_managed_object( - self, - managed_object: DbusInterfaceBaseAsync) -> None: - if self._dbus.attached_bus is None: - raise RuntimeError('Object manager not exported') - - removed_path = self._managed_object_to_path.pop(managed_object) - self._dbus.attached_bus.emit_object_removed(removed_path) diff --git a/src/sdbus/dbus_proxy_async_object_manager.py b/src/sdbus/dbus_proxy_async_object_manager.py new file mode 100644 index 0000000..33f86b1 --- /dev/null +++ b/src/sdbus/dbus_proxy_async_object_manager.py @@ -0,0 +1,130 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later + +# Copyright (C) 2020-2024 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 functools import partial +from typing import TYPE_CHECKING + +from .dbus_common_elements import DbusLocalObjectMeta +from .dbus_common_funcs import get_default_bus +from .dbus_proxy_async_interface_base import ( + DbusExportHandle, + DbusInterfaceBaseAsync, +) +from .dbus_proxy_async_interfaces import DbusInterfaceCommonAsync +from .dbus_proxy_async_method import dbus_method_async +from .dbus_proxy_async_signal import dbus_signal_async + +if TYPE_CHECKING: + from typing import Any, Callable, Dict, List, Optional, Tuple + + from .sd_bus_internals import SdBus, SdBusSlot + + +class DbusObjectManagerExportHandle(DbusExportHandle): + def __init__( + self, + local_meta: DbusLocalObjectMeta, + remove_object_call: Callable[[], None], + ): + super().__init__(local_meta) + self.remove_object_call = remove_object_call + + def stop(self) -> None: + super().stop() + self.remove_object_call() + + +class DbusObjectManagerInterfaceAsync( + DbusInterfaceCommonAsync, + interface_name='org.freedesktop.DBus.ObjectManager', + serving_enabled=False, +): + def __init__(self) -> None: + super().__init__() + self._object_manager_slot: Optional[SdBusSlot] = None + self._managed_object_to_path: Dict[DbusInterfaceBaseAsync, str] = {} + + @dbus_method_async(result_signature='a{oa{sa{sv}}}') + async def get_managed_objects( + self) -> Dict[str, Dict[str, Dict[str, Any]]]: + raise NotImplementedError + + @dbus_signal_async('oa{sa{sv}}') + def interfaces_added(self) -> Tuple[str, Dict[str, Dict[str, Any]]]: + raise NotImplementedError + + @dbus_signal_async('oao') + def interfaces_removed(self) -> Tuple[str, List[str]]: + raise NotImplementedError + + def export_to_dbus( + self, + object_path: str, + bus: Optional[SdBus] = None, + ) -> DbusExportHandle: + if bus is None: + bus = get_default_bus() + + export_handle = super().export_to_dbus( + object_path, + bus, + ) + slot = bus.add_object_manager(object_path) + self._object_manager_slot = slot + export_handle._dbus_slots.append(slot) + return export_handle + + def export_with_manager( + self, + object_path: str, + object_to_export: DbusInterfaceBaseAsync, + bus: Optional[SdBus] = None, + ) -> DbusObjectManagerExportHandle: + if self._object_manager_slot is None: + raise RuntimeError('ObjectManager not intitialized') + + if bus is None: + bus = get_default_bus() + + object_to_export.export_to_dbus( + object_path, + bus, + ) + meta = object_to_export._dbus + if not isinstance(meta, DbusLocalObjectMeta): + raise TypeError + handle = DbusObjectManagerExportHandle( + meta, + partial(self.remove_managed_object, object_to_export), + ) + bus.emit_object_added(object_path) + self._managed_object_to_path[object_to_export] = object_path + + return handle + + def remove_managed_object( + self, + managed_object: DbusInterfaceBaseAsync) -> None: + if self._dbus.attached_bus is None: + raise RuntimeError('Object manager not exported') + + removed_path = self._managed_object_to_path.pop(managed_object) + self._dbus.attached_bus.emit_object_removed(removed_path) diff --git a/src/sdbus/sd_bus_internals.py b/src/sdbus/sd_bus_internals.py index 594647b..699455f 100644 --- a/src/sdbus/sd_bus_internals.py +++ b/src/sdbus/sd_bus_internals.py @@ -59,6 +59,7 @@ def close(self) -> None: class SdBusInterface: + slot: Optional[SdBusSlot] method_list: List[object] method_dict: Dict[bytes, object] property_list: List[object] diff --git a/src/sdbus/sd_bus_internals_interface.c b/src/sdbus/sd_bus_internals_interface.c index 2b9b038..acc97d1 100644 --- a/src/sdbus/sd_bus_internals_interface.c +++ b/src/sdbus/sd_bus_internals_interface.c @@ -328,7 +328,8 @@ static PyMethodDef SdBusInterface_methods[] = { {NULL, NULL, 0, NULL}, }; -static PyMemberDef SdBusInterface_members[] = {{"method_list", T_OBJECT, offsetof(SdBusInterfaceObject, method_list), READONLY, NULL}, +static PyMemberDef SdBusInterface_members[] = {{"slot", T_OBJECT, offsetof(SdBusInterfaceObject, interface_slot), READONLY, NULL}, + {"method_list", T_OBJECT, offsetof(SdBusInterfaceObject, method_list), READONLY, NULL}, {"method_dict", T_OBJECT, offsetof(SdBusInterfaceObject, method_dict), READONLY, NULL}, {"property_list", T_OBJECT, offsetof(SdBusInterfaceObject, property_list), READONLY, NULL}, {"property_get_dict", T_OBJECT, offsetof(SdBusInterfaceObject, property_get_dict), READONLY, NULL}, diff --git a/test/test_object_manager.py b/test/test_object_manager.py index 2d0e835..541c28a 100644 --- a/test/test_object_manager.py +++ b/test/test_object_manager.py @@ -19,6 +19,7 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA from __future__ import annotations +from sdbus.exceptions import DbusUnknownObjectError from sdbus.unittest import IsolatedDbusTestCase from sdbus.utils import ( parse_get_managed_objects, @@ -350,3 +351,55 @@ def test_str(self) -> str: self.assertEqual(path, MANAGED_PATH) self.assertIsNone(python_class) + + async def test_main_export_handle(self) -> None: + await self.bus.request_name_async(CONNECTION_NAME, 0) + + object_manager = ObjectManagerTestInterface() + + object_manager_connection = ObjectManagerTestInterface.new_proxy( + CONNECTION_NAME, OBJECT_MANAGER_PATH) + + with object_manager.export_to_dbus(OBJECT_MANAGER_PATH): + self.assertIsInstance( + await object_manager_connection.get_managed_objects(), + dict, + ) + + with self.assertRaises(DbusUnknownObjectError): + self.assertIsInstance( + await object_manager_connection.get_managed_objects(), + dict, + ) + + async def test_secondary_export_handle(self) -> None: + await self.bus.request_name_async(CONNECTION_NAME, 0) + + object_manager = ObjectManagerTestInterface() + + object_manager_connection = ObjectManagerTestInterface.new_proxy( + CONNECTION_NAME, OBJECT_MANAGER_PATH) + object_manager.export_to_dbus(OBJECT_MANAGER_PATH) + + managed_object = ManagedInterface() + managed_proxy = ManagedInterface.new_proxy( + CONNECTION_NAME, MANAGED_PATH, + ) + + async with self.assertDbusSignalEmits( + object_manager_connection.interfaces_added + ) as added, self.assertDbusSignalEmits( + object_manager_connection.interfaces_removed + ) as removed, object_manager.export_with_manager( + MANAGED_PATH, managed_object, + ): + self.assertEqual( + await managed_proxy.test_int, + TEST_NUMBER, + ) + + self.assertEqual(added.output[0][0], MANAGED_PATH) + self.assertEqual(removed.output[0][0], MANAGED_PATH) + + with self.assertRaises(DbusUnknownObjectError): + await managed_proxy.test_int diff --git a/test/test_sdbus_async.py b/test/test_sdbus_async.py index 8d745bd..60f1ff0 100644 --- a/test/test_sdbus_async.py +++ b/test/test_sdbus_async.py @@ -953,3 +953,25 @@ async def test_extremely_large_string(self) -> None: # Check that calling regular methods still works. for _ in range(5): await test_object_connection.returns_none_method() + + async def test_export_handle(self) -> None: + test_object = TestInterface() + test_object_connection = TestInterface.new_proxy( + TEST_SERVICE_NAME, '/', + ) + with self.assertRaises(DbusUnknownObjectError): + await test_object_connection.returns_none_method() + + with test_object.export_to_dbus("/"): + await test_object_connection.returns_none_method() + + with self.assertRaises(DbusUnknownObjectError): + await test_object_connection.returns_none_method() + + test_object2 = TestInterface() + handle = test_object2.export_to_dbus("/") + await test_object_connection.returns_none_method() + handle.stop() + + with self.assertRaises(DbusUnknownObjectError): + await test_object_connection.returns_none_method() From b3c5401cbc8e2dd1da9b1aa8709bbdc44448d3ec Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sun, 17 Mar 2024 14:42:42 +0500 Subject: [PATCH 093/188] docs: Reword the export functions handle explanations First explain that the handle can be used to stop export and then describe two ways of doing that. --- docs/asyncio_api.rst | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/docs/asyncio_api.rst b/docs/asyncio_api.rst index 195756b..d041eb7 100644 --- a/docs/asyncio_api.rst +++ b/docs/asyncio_api.rst @@ -134,6 +134,10 @@ Classes to remove the object from D-Bus or ``.stop()`` method of the handle can be called to remove object from D-Bus. + Returns a handle that can be used to remove object from D-Bus + by either using it as a context manager or by calling ``.stop()`` + method of the handle. + .. code-block:: python with dbus_object.export_to_dbus("/"): @@ -242,10 +246,10 @@ Classes ObjectManager will keep the reference to the object. - Returns a handle that can either be used as a context manager - to remove the object or ``.stop()`` method of the handle can be - called to remove object from D-Bus and drop reference to the object. - Signal will be emitted when the object is stopped via handle. + Returns a handle that can be used to remove object from D-Bus and + drop reference to it by either using it as a context manager or + by calling ``.stop()`` method of the handle. Signal will be emitted + once the object is stopped being exported. .. code-block:: python From bc50226e464405fc42bae35e9c7c0055a9a28366 Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sun, 17 Mar 2024 14:50:13 +0500 Subject: [PATCH 094/188] docs: Fix parse_get_managed_objects arguments description It was missing `on_unknown_member` description, the second argument had copy pasted name `interfaces_added_data`. Also add the link to it in the `DbusObjectManagerInterfaceAsync.get_managed_objects` description. --- docs/asyncio_api.rst | 3 +++ docs/utils.rst | 7 +++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/docs/asyncio_api.rst b/docs/asyncio_api.rst index d041eb7..39b0221 100644 --- a/docs/asyncio_api.rst +++ b/docs/asyncio_api.rst @@ -192,6 +192,9 @@ Classes Get the objects this object manager in managing. + :py:func:`sdbus.utils.parse_get_managed_objects` can be used + to make returned data easier to work with. + :return: Triple nested dictionary that contains all the objects paths with their properties values. diff --git a/docs/utils.rst b/docs/utils.rst index abdc922..9dcf840 100644 --- a/docs/utils.rst +++ b/docs/utils.rst @@ -70,10 +70,13 @@ Parsing utilities :param Iterable[DbusInterfaceBaseAsync] interfaces: Possible interfaces of the managed objects. Can accept classes with multiple interfaces defined. - :param Dict interfaces_added_data: Data returned by ``get_managed_objects`` call. - :param str on_unknown_member: If an unknown D-Bus interface was encountered + :param Dict managed_objects_data: Data returned by ``get_managed_objects`` call. + :param str on_unknown_interface: If an unknown D-Bus interface was encountered either raise an ``"error"`` (default) or return ``"none"`` instead of interface class. + :param str on_unknown_member: If an unknown D-Bus property was encountered + either raise an ``"error"`` (default), ``"ignore"`` the property + or ``"reuse"`` the D-Bus name for the member. :rtype: Dict[str, Tuple[Optional[Type[DbusInterfaceBaseAsync], Dict[str, Any]]]] :returns: Dictionary where keys are paths and values are tuples of managed objects classes and their properties data. From 2182d26e58b89dfd869c7dde51bc2314089a7e3a Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sun, 17 Mar 2024 15:11:04 +0500 Subject: [PATCH 095/188] actions: Add ability to specify the version to install from PyPI Because RC versions are not installed by default a specifier must be used. --- .github/workflows/ubuntu_pypi_test.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ubuntu_pypi_test.yml b/.github/workflows/ubuntu_pypi_test.yml index 9a1bc6c..816008d 100644 --- a/.github/workflows/ubuntu_pypi_test.yml +++ b/.github/workflows/ubuntu_pypi_test.yml @@ -2,6 +2,9 @@ name: Install package from PyPI and run unit tests on Ubuntu 20.04 on: workflow_dispatch: + inputs: + pypi_version: + description: "Version specifier to install from PyPI" jobs: run: @@ -17,7 +20,9 @@ jobs: systemd dbus python3 python3-pip python3-jinja2 - name: Install package run: | - sudo pip3 install sdbus>=0.8rc2 + sudo pip3 install "sdbus ${SDBUS_VERSION}" + env: + SDBUS_VERSION: ${{ inputs.pypi_version }} - name: List package run: | pip3 list | grep sdbus From fb50cadf67bbb5761f9f02b1b84a002c0081e135 Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sun, 17 Mar 2024 18:23:45 +0500 Subject: [PATCH 096/188] wheel-build: Unpack source archive inside the container Otherwise it would be possible for source files to not fully update when switching versions. --- wheel-build/build_container_archive.py | 58 ++++++++++-------------- wheel-build/run_inside_container.py | 61 +++++++++++++++++++------- 2 files changed, 68 insertions(+), 51 deletions(-) diff --git a/wheel-build/build_container_archive.py b/wheel-build/build_container_archive.py index 53bc699..90b85ef 100755 --- a/wheel-build/build_container_archive.py +++ b/wheel-build/build_container_archive.py @@ -25,42 +25,33 @@ from pathlib import Path from shutil import copy from subprocess import PIPE, run -from tempfile import TemporaryDirectory -SYSTEMD_VERSION = '249.14' +SYSTEMD_VERSION = '249.17' UTIL_LINUX_VERSION = '2.37' NINJA_VERSION = '1.10.2' -LIBCAP_VERSION = '2.64' +LIBCAP_VERSION = '2.69' def create_archive(build_root: Path, output_file: Path) -> None: run( - ['tar', '--create', '--file', str(output_file.absolute()), '.'], + [ + 'tar', '--create', + '--file', str(output_file.absolute()), + '.', + ], cwd=build_root.resolve(), check=True, ) -def download_and_unpack_source(target_dir: Path, url: str) -> None: - target_dir.mkdir(exist_ok=True) # TODO: maybe delete folder - - with TemporaryDirectory() as tmpdir: - tmpdir_path = Path(tmpdir) - dowload_tar_path = tmpdir_path / 'donwload.tar.gz' - - run( - ['curl', '--fail', '--location', - url, '--output', str(dowload_tar_path)], - check=True, - ) - - run( - ['tar', - '--directory', str(target_dir), - '--strip-components=1', - '--extract', '--file', str(dowload_tar_path)], - check=True, - ) +def download_source(target: Path, url: str) -> None: + run( + [ + 'curl', '--fail', '--location', + url, '--output', str(target) + ], + check=True, + ) def download_systemd_source(build_dir: Path) -> None: @@ -68,31 +59,30 @@ def download_systemd_source(build_dir: Path) -> None: "https://github.com/systemd/systemd-stable/" f"archive/refs/tags/v{SYSTEMD_VERSION}.tar.gz" ) - systemd_src_dir = build_dir / "src_systemd" - systemd_src_dir.mkdir(exist_ok=True) + systemd_download_file = build_dir / "systemd.tar.gz" - util_linux_url = ( + util_linux_src_url = ( "https://mirrors.edge.kernel.org/pub/linux/utils/util-linux/" f"v{UTIL_LINUX_VERSION}/util-linux-{UTIL_LINUX_VERSION}.tar.xz" ) - util_linux_src_dir = build_dir / "src_util_linux" + util_linux_download_file = build_dir / "util_linux.tar.xz" ninja_src_url = ( "https://github.com/ninja-build/ninja/" f"archive/refs/tags/v{NINJA_VERSION}.tar.gz" ) - ninja_src_dir = build_dir / "src_ninja" + ninja_download_file = build_dir / "ninja.tar.gz" libcap_src_url = ( "https://kernel.org/pub/linux/libs/security/" f"linux-privs/libcap2/libcap-{LIBCAP_VERSION}.tar.xz" ) - libcap_src_dir = build_dir / 'src_libcap' + libcap_download_file = build_dir / 'libcap.tar.xz' - download_and_unpack_source(systemd_src_dir, systemd_download_url) - download_and_unpack_source(util_linux_src_dir, util_linux_url) - download_and_unpack_source(ninja_src_dir, ninja_src_url) - download_and_unpack_source(libcap_src_dir, libcap_src_url) + download_source(systemd_download_file, systemd_download_url) + download_source(util_linux_download_file, util_linux_src_url) + download_source(ninja_download_file, ninja_src_url) + download_source(libcap_download_file, libcap_src_url) def copy_git_ls_files(source_root: Path, build_root: Path) -> None: diff --git a/wheel-build/run_inside_container.py b/wheel-build/run_inside_container.py index be7ad59..41ec83b 100755 --- a/wheel-build/run_inside_container.py +++ b/wheel-build/run_inside_container.py @@ -60,6 +60,37 @@ '-fstack-clash-protection', ] +NINJA_ARCHIVE = ROOT_DIR / "ninja.tar.gz" +NINJA_SRC_PATH = ROOT_DIR / 'src_ninja' + +UTIL_LINUX_ARCHIVE = ROOT_DIR / "util_linux.tar.xz" +UTIL_LINUX_SRC_PATH = ROOT_DIR / 'src_util_linux' + +LIBCAP_ARCHIVE = ROOT_DIR / "libcap.tar.xz" +LIBCAP_SRC_PATH = ROOT_DIR / 'src_libcap' + +SYSTEMD_ARCHIVE = ROOT_DIR / "systemd.tar.gz" +SYSTEMD_SRC_PATH = ROOT_DIR / 'src_systemd' + + +def unpack_archives() -> None: + for archive, to in ( + (NINJA_ARCHIVE, NINJA_SRC_PATH), + (UTIL_LINUX_ARCHIVE, UTIL_LINUX_SRC_PATH), + (LIBCAP_ARCHIVE, LIBCAP_SRC_PATH), + (SYSTEMD_ARCHIVE, SYSTEMD_SRC_PATH), + ): + to.mkdir(exist_ok=True) + run( + [ + "tar", "--verbose", + "--directory", str(to), + "--strip-components=1", + "--extract", "--file", str(archive) + ], + check=True, + ) + def setup_env() -> None: python_bin_paths = (f"/opt/python/{x}/bin" for x in PYTHON_VERSIONS) @@ -104,70 +135,65 @@ def install_packages() -> None: def install_ninja() -> None: - ninja_src_path = ROOT_DIR / 'src_ninja' - ninja_boot_strap_path = ninja_src_path / 'configure.py' + + ninja_boot_strap_path = NINJA_SRC_PATH / 'configure.py' run( [ninja_boot_strap_path, '--bootstrap'], - cwd=ninja_src_path, + cwd=NINJA_SRC_PATH, check=True, ) - copy(ninja_src_path / 'ninja', '/usr/local/bin') + copy(NINJA_SRC_PATH / 'ninja', '/usr/local/bin') def install_meson() -> None: run( - ['pip3', 'install', 'meson==0.62', 'Jinja2==3.1.1'], + ['pip3', 'install', 'meson==1.4.0', 'Jinja2==3.1.1'], check=True, ) def install_util_linux() -> None: - util_linux_src_path = ROOT_DIR / 'src_util_linux' - run( - [util_linux_src_path / 'autogen.sh'], - cwd=util_linux_src_path, + [UTIL_LINUX_SRC_PATH / 'autogen.sh'], + cwd=UTIL_LINUX_SRC_PATH, env={'AL_OPTS': '-I/usr/share/aclocal/', **environ}, check=True, ) run( [ - util_linux_src_path / 'configure', + UTIL_LINUX_SRC_PATH / 'configure', '--prefix', '/usr/local', '--libdir', '/usr/local/lib64', '--enable-symvers', ], - cwd=util_linux_src_path, + cwd=UTIL_LINUX_SRC_PATH, check=True, ) run( ['make', '--jobs', NPROC, 'install'], - cwd=util_linux_src_path, + cwd=UTIL_LINUX_SRC_PATH, check=True, ) def install_libcap() -> None: - libcap_src_path = ROOT_DIR / 'src_libcap' - run( ['make', '--jobs', NPROC, 'install'], - cwd=libcap_src_path, + cwd=LIBCAP_SRC_PATH, check=True, ) def install_systemd() -> None: - systemd_src_path = ROOT_DIR / 'src_systemd' systemd_build_path = ROOT_DIR / 'build_systemd' run( ['meson', 'setup', - systemd_build_path, systemd_src_path, + systemd_build_path, SYSTEMD_SRC_PATH, '-Dstatic-libsystemd=pic', '-Dtests=false', '--buildtype', 'plain', @@ -227,6 +253,7 @@ def drop_to_shell() -> None: def main() -> None: + unpack_archives() setup_env() install_packages() From 77a9fd021b0fbd30f31eed3557b767951850564b Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sun, 17 Mar 2024 19:49:43 +0500 Subject: [PATCH 097/188] wheel-build: Disable more systemd options when building wheel No point in compiling source code that won't be used. --- wheel-build/run_inside_container.py | 33 +++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/wheel-build/run_inside_container.py b/wheel-build/run_inside_container.py index 41ec83b..8b7fe75 100755 --- a/wheel-build/run_inside_container.py +++ b/wheel-build/run_inside_container.py @@ -60,6 +60,36 @@ '-fstack-clash-protection', ] +SYSTEMD_OPTIONS: List[str] = [ + "static-libsystemd=pic", + "tests=false", + "coredump=false", + "dbus=false", + "efi=false", + "elfutils=false", + "hostnamed=false", + "homed=false", + "importd=false", + "initrd=false", + "kernel-install=false", + "logind=false", + "machined=false", + "man=false", + "networkd=false", + "portabled=false", + "repart=false", + "sysext=false", + "sysusers=false", + "timedated=false", + "timesyncd=false", + "tmpfiles=false", + "oomd=false", + "hibernate=false", + "nss-systemd=false", + "nss-resolve=false", +] + + NINJA_ARCHIVE = ROOT_DIR / "ninja.tar.gz" NINJA_SRC_PATH = ROOT_DIR / 'src_ninja' @@ -194,10 +224,9 @@ def install_systemd() -> None: run( ['meson', 'setup', systemd_build_path, SYSTEMD_SRC_PATH, - '-Dstatic-libsystemd=pic', - '-Dtests=false', '--buildtype', 'plain', '-Db_lto=true', '-Db_pie=true', + *(f"-D{o}" for o in SYSTEMD_OPTIONS) ], env={**environ, 'PKG_CONFIG_PATH': '/usr/local/lib64/pkgconfig'}, check=True, From 339a3d28f5e269ccc268cee61585262c8a6f9631 Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sun, 17 Mar 2024 20:03:47 +0500 Subject: [PATCH 098/188] docs: Add note that setter_private can be used in overrides in 0.12.0 --- docs/asyncio_api.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/asyncio_api.rst b/docs/asyncio_api.rst index 39b0221..c94cedc 100644 --- a/docs/asyncio_api.rst +++ b/docs/asyncio_api.rst @@ -462,6 +462,8 @@ Decorators :py:attr:`properties_changed ` signal to D-Bus. + *Changed in version 0.12.0:* can now be used in overrides. + .. py:method:: get_async() :async: From 95751a77aaf281e2156e8c9294226fa8426c984a Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sun, 17 Mar 2024 20:37:04 +0500 Subject: [PATCH 099/188] Version 0.12.RC1 --- CHANGELOG.md | 28 ++++++++++++++++++++++++++++ setup.py | 2 +- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f286cd0..2fa73ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,31 @@ +## 0.12.RC1 + +This version significantly reworked the internal undocumented classes +and functions. If you used the undocumented API you would probably need +to adjust your code. Type checker like `mypy` can be very useful for this. + +### Features: + +* `@setter_private` can now be used in overrides. +* Added `assertDbusSignalEmits` method to `IsolatedDbusTestCase`. + Can be used to assert that a D-Bus signal was emitted + inside the `async with` block. +* Added `sdbus.utils.parse_get_managed_objects` function. Can be + used to parse the ObjectManager's `get_managed_objects` method + data to classes and Python attribute names. +* Added a handle that is returned by `export_to_dbus` and `export_with_manager` + methods. This handle can be used to explicitly control when object is accessible + from D-Bus. (requested by @dragomirecky) + +### Fixes: + +* Fixed async D-Bus properties not having a proper generic typing. (reported by @ValdezFOmar) +* Fixed build not working when systemd has a minor version suffix. +* Fixed being unable to name arguments in D-Bus introspection when + method has no return arguments. (reported by @colazzo) +* Fixed serving D-Bus methods that return a single struct. (reported by @colazzo) +* Fixed sending extremely large D-Bus messages getting stuck. (reported by @colazzo) + ## 0.11.1 ### Features: diff --git a/setup.py b/setup.py index 355962d..f1ddc58 100644 --- a/setup.py +++ b/setup.py @@ -96,7 +96,7 @@ def get_link_arguments() -> List[str]: 'Based on sd-bus from libsystemd.'), long_description=long_description, long_description_content_type='text/markdown', - version='0.11.1', + version='0.12.rc1', url='https://github.com/igo95862/python-sdbus', author='igo95862', author_email='igo95862@yandex.ru', From 1a3410dadaf5f4933e7dc81540eb66578c708fb6 Mon Sep 17 00:00:00 2001 From: Alexander Pushkov Date: Wed, 20 Mar 2024 12:43:42 +0900 Subject: [PATCH 100/188] docs: fix typo in example filename --- docs/examples.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/examples.rst b/docs/examples.rst index f4bc9c3..22aa695 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -10,7 +10,7 @@ There are 3 files: * ``example_interface.py`` File that contains the interface definition. * ``example_server.py`` Server. -* ``example_interface.py`` Client. +* ``example_client.py`` Client. ``example_interface.py`` file: :: From 6545ea0da56943c970b204b8d5dd23afb0975a7c Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sat, 23 Mar 2024 15:28:18 +0500 Subject: [PATCH 101/188] Add unique prefix to temporary dirs created by IsolatedDbusTestCase --- src/sdbus/unittest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sdbus/unittest.py b/src/sdbus/unittest.py index 63a0d47..0df3465 100644 --- a/src/sdbus/unittest.py +++ b/src/sdbus/unittest.py @@ -175,7 +175,7 @@ class IsolatedDbusTestCase(IsolatedAsyncioTestCase): dbus_executable_name: ClassVar[str] = 'dbus-daemon' def setUp(self) -> None: - self.temp_dir = TemporaryDirectory() + self.temp_dir = TemporaryDirectory(prefix="python-sdbus-") self.temp_dir_path = Path(self.temp_dir.name) self.dbus_socket_path = self.temp_dir_path / 'test_dbus.socket' From 55d2667f0f7550ebee0b572f0200594ef6c3e93e Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sat, 23 Mar 2024 16:43:16 +0500 Subject: [PATCH 102/188] Version 0.12.0 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index f1ddc58..f11dd69 100644 --- a/setup.py +++ b/setup.py @@ -96,7 +96,7 @@ def get_link_arguments() -> List[str]: 'Based on sd-bus from libsystemd.'), long_description=long_description, long_description_content_type='text/markdown', - version='0.12.rc1', + version='0.12.0', url='https://github.com/igo95862/python-sdbus', author='igo95862', author_email='igo95862@yandex.ru', From ecaadf474c89a929b63a212045c72f4f9a42cb4e Mon Sep 17 00:00:00 2001 From: igo95862 Date: Fri, 10 May 2024 23:24:10 +0500 Subject: [PATCH 103/188] Fix small typo in client code example --- README.md | 2 +- examples/simple/client.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f7c3c0a..08bc9c5 100644 --- a/README.md +++ b/README.md @@ -200,7 +200,7 @@ async def get_hello_world() -> None: loop = new_event_loop() -# Always binds your tasks to a variable +# Always bind your tasks to a variable task_upper = loop.create_task(call_upper()) task_clock = loop.create_task(print_clock()) task_hello_world = loop.create_task(get_hello_world()) diff --git a/examples/simple/client.py b/examples/simple/client.py index c541996..59a0ca8 100644 --- a/examples/simple/client.py +++ b/examples/simple/client.py @@ -46,7 +46,7 @@ async def get_hello_world() -> None: loop = new_event_loop() -# Always binds your tasks to a variable +# Always bind your tasks to a variable task_upper = loop.create_task(call_upper()) task_clock = loop.create_task(print_clock()) task_hello_world = loop.create_task(get_hello_world()) From c2b046e44422cbf4d0ecb37af52a5ac9f82691ab Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sun, 26 May 2024 22:48:09 +0500 Subject: [PATCH 104/188] docs: Replace D-Feet reference with D-Spy Apparently D-Feet is deprecated and is no longer updated. --- docs/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.rst b/docs/index.rst index 0735323..21b5646 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -24,7 +24,7 @@ If you are unfamiliar with D-Bus you might want to read following pages: `D-Bus specification by freedesktop.org `_ -`Install D-Feet D-Bus debugger and observe services and objects on your D-Bus `_ +`Install D-Spy D-Bus debugger and observe services and objects on your D-Bus `_ .. toctree:: From 47ee3449951b6d3ad587e2ab56ea055c6bf85eea Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sun, 2 Jun 2024 21:38:55 +0500 Subject: [PATCH 105/188] Run CI for all branches Not sure why it was limited to master branch in first place --- .github/workflows/ubuntu_test.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/ubuntu_test.yml b/.github/workflows/ubuntu_test.yml index 261fcc7..ac4d2a5 100644 --- a/.github/workflows/ubuntu_test.yml +++ b/.github/workflows/ubuntu_test.yml @@ -2,7 +2,6 @@ name: CI on: push: - branches: [master] pull_request: workflow_dispatch: From 9e53e06bfafe35695536b483e042c6dc5918c0b7 Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sat, 10 Aug 2024 20:43:19 +0100 Subject: [PATCH 106/188] Use ExitStack for IsolatedDbusTestCase Allows reusing the D-Bus daemon setup code and is more robust if an exception happens during the setup. Will be used in the future for the benchmarks. --- src/sdbus/unittest.py | 87 +++++++++++++++++++++++++++---------------- 1 file changed, 55 insertions(+), 32 deletions(-) diff --git a/src/sdbus/unittest.py b/src/sdbus/unittest.py index 0df3465..3117382 100644 --- a/src/sdbus/unittest.py +++ b/src/sdbus/unittest.py @@ -20,6 +20,8 @@ from __future__ import annotations from asyncio import Event, TimeoutError, wait_for +from contextlib import ExitStack, contextmanager +from operator import setitem from os import environ, kill from pathlib import Path from signal import SIGTERM @@ -42,6 +44,7 @@ Any, AsyncContextManager, ClassVar, + Iterator, List, Optional, TypeVar, @@ -171,52 +174,72 @@ async def __aenter__(self) -> DbusSignalRecorderBase: return self -class IsolatedDbusTestCase(IsolatedAsyncioTestCase): - dbus_executable_name: ClassVar[str] = 'dbus-daemon' - - def setUp(self) -> None: - self.temp_dir = TemporaryDirectory(prefix="python-sdbus-") - self.temp_dir_path = Path(self.temp_dir.name) - - self.dbus_socket_path = self.temp_dir_path / 'test_dbus.socket' - self.pid_path = self.temp_dir_path / 'dbus.pid' - - self.dbus_config_file = self.temp_dir_path / 'dbus.config' +@contextmanager +def _isolated_dbus( + dbus_executable_name: str = "dbus-daemon", +) -> Iterator[SdBus]: + with ExitStack() as exit_stack: + temp_dir_path = Path( + exit_stack.enter_context( + TemporaryDirectory(prefix="python-sdbus-") + ) + ) - with open(self.dbus_config_file, mode='x') as conf_file: - conf_file.write(dbus_config.format( - socket_path=self.dbus_socket_path, - pidfile_path=self.pid_path)) + dbus_socket_path = temp_dir_path / "test_dbus.socket" + pid_path = temp_dir_path / "dbus.pid" + dbus_config_file = temp_dir_path / "dbus.config" + dbus_config_file.write_text( + dbus_config.format( + socket_path=dbus_socket_path, + pidfile_path=pid_path + ) + ) subprocess_run( args=( - self.dbus_executable_name, - '--config-file', self.dbus_config_file, + dbus_executable_name, + '--config-file', dbus_config_file, '--fork', ), stdin=DEVNULL, check=True, ) + # D-Bus daemon exits once it forks and is initialized. + + dbus_pid = int(pid_path.read_text()) + exit_stack.callback(kill, dbus_pid, SIGTERM) + + old_session_bus_address = environ.get("DBUS_SESSION_BUS_ADDRESS") + if old_session_bus_address is not None: + exit_stack.callback( + setitem, + environ, + "DBUS_SESSION_BUS_ADDRESS", + old_session_bus_address, + ) + else: + exit_stack.callback( + environ.pop, + "DBUS_SESSION_BUS_ADDRESS", + ) + environ["DBUS_SESSION_BUS_ADDRESS"] = f"unix:path={dbus_socket_path}" - self.old_session_bus_address = environ.get('DBUS_SESSION_BUS_ADDRESS') - environ[ - 'DBUS_SESSION_BUS_ADDRESS'] = f"unix:path={self.dbus_socket_path}" + bus = sd_bus_open_user() + set_default_bus(bus) + yield bus - self.bus = sd_bus_open_user() - set_default_bus(self.bus) - async def asyncSetUp(self) -> None: - set_default_bus(self.bus) +class IsolatedDbusTestCase(IsolatedAsyncioTestCase): + dbus_executable_name: ClassVar[str] = 'dbus-daemon' - def tearDown(self) -> None: - with open(self.pid_path) as pid_file: - dbus_pid = int(pid_file.read()) + def setUp(self) -> None: + # TODO: Use enterContext from Python 3.11 + _isolated_dbus_cm = _isolated_dbus() + self.bus = _isolated_dbus_cm.__enter__() + self.addCleanup(_isolated_dbus_cm.__exit__, None, None, None) - kill(dbus_pid, SIGTERM) - self.temp_dir.cleanup() - environ.pop('DBUS_SESSION_BUS_ADDRESS') - if self.old_session_bus_address is not None: - environ['DBUS_SESSION_BUS_ADDRESS'] = self.old_session_bus_address + async def asyncSetUp(self) -> None: + set_default_bus(self.bus) def assertDbusSignalEmits( self, From 57bd774e99564811ab19076b2c12e80f8487e83b Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sat, 10 Aug 2024 20:50:26 +0100 Subject: [PATCH 107/188] Fix DbusPropertyOverride typing Define setter override as `Callable[[Any], T]`. Make both DbusMethodOverride and DbusPropertyOverride Generic as it might be useful in the future. --- src/sdbus/dbus_common_elements.py | 12 ++++++------ src/sdbus/dbus_proxy_async_interface_base.py | 7 ++++--- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/sdbus/dbus_common_elements.py b/src/sdbus/dbus_common_elements.py index f1a3ccb..7c8aa20 100644 --- a/src/sdbus/dbus_common_elements.py +++ b/src/sdbus/dbus_common_elements.py @@ -20,7 +20,7 @@ from __future__ import annotations from inspect import getfullargspec -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Generic, TypeVar from .dbus_common_funcs import ( _is_property_flags_correct, @@ -40,14 +40,14 @@ Sequence, Tuple, Type, - TypeVar, ) - T = TypeVar('T') SelfMeta = TypeVar('SelfMeta', bound="DbusInterfaceMetaCommon") from .sd_bus_internals import SdBus, SdBusInterface +T = TypeVar('T') + class DbusSomethingCommon: interface_name: str @@ -299,13 +299,13 @@ class DbusBindedSync: ... -class DbusMethodOverride: +class DbusMethodOverride(Generic[T]): def __init__(self, override_method: T): self.override_method = override_method -class DbusPropertyOverride: - def __init__(self, getter_override: T): +class DbusPropertyOverride(Generic[T]): + def __init__(self, getter_override: Callable[[Any], T]): self.getter_override = getter_override self.setter_override: Optional[Callable[[Any, T], None]] = None self.is_setter_public = True diff --git a/src/sdbus/dbus_proxy_async_interface_base.py b/src/sdbus/dbus_proxy_async_interface_base.py index 2647897..f13d162 100644 --- a/src/sdbus/dbus_proxy_async_interface_base.py +++ b/src/sdbus/dbus_proxy_async_interface_base.py @@ -64,8 +64,9 @@ from .dbus_common_elements import DbusBindedAsync from .sd_bus_internals import SdBus, SdBusSlot + T = TypeVar('T') Self = TypeVar('Self', bound="DbusInterfaceBaseAsync") - DbusOverride = Union[DbusMethodOverride, DbusPropertyOverride] + DbusOverride = Union[DbusMethodOverride[T], DbusPropertyOverride[T]] DBUS_CLASS_TO_META: WeakKeyDictionary[ @@ -79,7 +80,7 @@ class DbusInterfaceMetaAsync(DbusInterfaceMetaCommon): @staticmethod def _process_dbus_method_override( override_attr_name: str, - override: DbusMethodOverride, + override: DbusMethodOverride[T], mro_dbus_elements: Dict[str, DbusSomethingAsync], ) -> DbusMethodAsync: try: @@ -103,7 +104,7 @@ def _process_dbus_method_override( @staticmethod def _process_dbus_property_override( override_attr_name: str, - override: DbusPropertyOverride, + override: DbusPropertyOverride[T], mro_dbus_elements: Dict[str, DbusSomethingAsync], ) -> DbusPropertyAsync[Any]: try: From a792094f019366afc7b12ae329aab9953ba508b4 Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sun, 11 Aug 2024 10:35:55 +0100 Subject: [PATCH 108/188] Remove undocumented IsolatedDbusTestCase.dbus_executable_name attr Maybe reimplemented and documented in the future. --- src/sdbus/unittest.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/sdbus/unittest.py b/src/sdbus/unittest.py index 3117382..90702e8 100644 --- a/src/sdbus/unittest.py +++ b/src/sdbus/unittest.py @@ -43,7 +43,6 @@ from typing import ( Any, AsyncContextManager, - ClassVar, Iterator, List, Optional, @@ -230,8 +229,6 @@ def _isolated_dbus( class IsolatedDbusTestCase(IsolatedAsyncioTestCase): - dbus_executable_name: ClassVar[str] = 'dbus-daemon' - def setUp(self) -> None: # TODO: Use enterContext from Python 3.11 _isolated_dbus_cm = _isolated_dbus() From 3ca9e7cfe14c6e967f2c0d8b5db8c8bf9901347c Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sun, 11 Aug 2024 15:39:43 +0100 Subject: [PATCH 109/188] Use --recursive option for autopep8 instead of passing exact files When a new folder will be added autopep8 will automatically descend in to it instead of having to pass new folder manually. --- tools/run_py_linters.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tools/run_py_linters.py b/tools/run_py_linters.py index adff96e..1312fde 100755 --- a/tools/run_py_linters.py +++ b/tools/run_py_linters.py @@ -103,10 +103,9 @@ def get_all_python_files() -> List[Path]: def formater_main() -> None: - all_python_files = get_all_python_files() run( - args=('autopep8', '--in-place', *all_python_files), + args=('autopep8', '--recursive', '--in-place', *all_python_modules), check=True, ) @@ -115,7 +114,7 @@ def formater_main() -> None: 'isort', '-m', 'VERTICAL_HANGING_INDENT', '--trailing-comma', - *all_python_files, + *all_python_modules, ), check=True, ) From 7e970a6f213c2ba1bba48f7b5b558eabe4f15747 Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sun, 11 Aug 2024 15:53:48 +0100 Subject: [PATCH 110/188] Add initial benchmarks Use pyperf library. Benchmarks added: * `sdbus_async_ping`: Ping D-Bus daemon in sequence * `sdbus_async_ping_gather`: Ping D-Bus daemon in parallel using `asyncio.gather`. --- test/benchmarks/__init__.py | 20 ++++++++ test/benchmarks/bench_async_ping.py | 80 +++++++++++++++++++++++++++++ 2 files changed, 100 insertions(+) create mode 100644 test/benchmarks/__init__.py create mode 100644 test/benchmarks/bench_async_ping.py diff --git a/test/benchmarks/__init__.py b/test/benchmarks/__init__.py new file mode 100644 index 0000000..d1a5ccb --- /dev/null +++ b/test/benchmarks/__init__.py @@ -0,0 +1,20 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later + +# Copyright (C) 2024 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 diff --git a/test/benchmarks/bench_async_ping.py b/test/benchmarks/bench_async_ping.py new file mode 100644 index 0000000..06813e3 --- /dev/null +++ b/test/benchmarks/bench_async_ping.py @@ -0,0 +1,80 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later + +# Copyright (C) 2024 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 asyncio import gather +from asyncio import run as asyncio_run +from time import perf_counter + +import pyperf # type: ignore +from sdbus.unittest import _isolated_dbus + +from sdbus import DbusInterfaceCommonAsync + + +def bench_async_ping_gather(loops: int) -> float: + with _isolated_dbus() as bus: + dbus_interface = DbusInterfaceCommonAsync.new_proxy( + "org.freedesktop.DBus", + "/org/freedesktop/DBus", + bus, + ) + + async def run_ping_gather() -> float: + + gather_ping = gather( + *(dbus_interface.dbus_ping() for _ in range(loops)) + ) + start = perf_counter() + + await gather_ping + + return perf_counter() - start + + return asyncio_run(run_ping_gather()) + + +def bench_async_ping(loops: int) -> float: + with _isolated_dbus() as bus: + dbus_interface = DbusInterfaceCommonAsync.new_proxy( + "org.freedesktop.DBus", + "/org/freedesktop/DBus", + bus, + ) + + async def run_ping() -> float: + start = perf_counter() + + for _ in range(loops): + await dbus_interface.dbus_ping() + + return perf_counter() - start + + return asyncio_run(run_ping()) + + +def main() -> None: + runner = pyperf.Runner() + runner.bench_time_func('sdbus_async_ping', bench_async_ping) + runner.bench_time_func('sdbus_async_ping_gather', bench_async_ping_gather) + + +if __name__ == "__main__": + main() From 2cd10e61972377f7772c30a0ae81bd2a5d2bebd9 Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sun, 18 Aug 2024 20:32:54 +0100 Subject: [PATCH 111/188] Reformat with recent clang-format --- src/sdbus/sd_bus_internals_interface.c | 27 ++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/src/sdbus/sd_bus_internals_interface.c b/src/sdbus/sd_bus_internals_interface.c index acc97d1..ac9ddc4 100644 --- a/src/sdbus/sd_bus_internals_interface.c +++ b/src/sdbus/sd_bus_internals_interface.c @@ -224,10 +224,11 @@ static PyObject* SdBusInterface_create_vtable(SdBusInterfaceObject* self, PyObje self->vtable[0] = start_vtable; Py_ssize_t current_index = 1; // Iter method definitions - for (Py_ssize_t i = 0; i < num_of_methods; ({ - ++i; - ++current_index; - })) { + for (Py_ssize_t i = 0; i < num_of_methods; ( + { + ++i; + ++current_index; + })) { PyObject* method_tuple = CALL_PYTHON_AND_CHECK(PyList_GetItem(self->method_list, i)); PyObject* method_name_object = CALL_PYTHON_AND_CHECK(PyTuple_GetItem(method_tuple, 0)); @@ -251,10 +252,11 @@ static PyObject* SdBusInterface_create_vtable(SdBusInterfaceObject* self, PyObje self->vtable[current_index] = temp_vtable; } - for (Py_ssize_t i = 0; i < num_of_properties; ({ - ++i; - ++current_index; - })) { + for (Py_ssize_t i = 0; i < num_of_properties; ( + { + ++i; + ++current_index; + })) { PyObject* property_tuple = SD_BUS_PY_LIST_GET_ITEM(self->property_list, i); PyObject* property_name_str = SD_BUS_PY_TUPLE_GET_ITEM(property_tuple, 0); @@ -290,10 +292,11 @@ static PyObject* SdBusInterface_create_vtable(SdBusInterfaceObject* self, PyObje } } - for (Py_ssize_t i = 0; i < num_of_signals; ({ - ++i; - ++current_index; - })) { + for (Py_ssize_t i = 0; i < num_of_signals; ( + { + ++i; + ++current_index; + })) { PyObject* signal_tuple = SD_BUS_PY_LIST_GET_ITEM(self->signal_list, i); PyObject* signal_name_str = SD_BUS_PY_TUPLE_GET_ITEM(signal_tuple, 0); From a79a6557ac47a3ee15ba05423a15f466847c7cde Mon Sep 17 00:00:00 2001 From: igo95862 Date: Mon, 9 Sep 2024 21:48:41 +0100 Subject: [PATCH 112/188] Bind SdBus to an async loop on first async use All awaitables produced will belong to that loop. This prevents users from mixing different asyncio loops which results in awaitables that never complete because all file descriptor watchers are registered on another loop. Now a `RuntimeError` will be raised. --- src/sdbus/sd_bus_internals.h | 1 + src/sdbus/sd_bus_internals_bus.c | 16 ++++++++++++---- test/test_sdbus_async.py | 18 +++++++++++++++++- 3 files changed, 30 insertions(+), 5 deletions(-) diff --git a/src/sdbus/sd_bus_internals.h b/src/sdbus/sd_bus_internals.h index 2694351..b6d7467 100644 --- a/src/sdbus/sd_bus_internals.h +++ b/src/sdbus/sd_bus_internals.h @@ -333,6 +333,7 @@ typedef struct { PyObject_HEAD; sd_bus* sd_bus_ref; PyObject* bus_fd; + PyObject* loop; int asyncio_watchers_last_state; } SdBusObject; diff --git a/src/sdbus/sd_bus_internals_bus.c b/src/sdbus/sd_bus_internals_bus.c index 759a914..17394a9 100644 --- a/src/sdbus/sd_bus_internals_bus.c +++ b/src/sdbus/sd_bus_internals_bus.c @@ -25,6 +25,7 @@ static void SdBus_dealloc(SdBusObject* self) { sd_bus_unref(self->sd_bus_ref); Py_XDECREF(self->bus_fd); + Py_XDECREF(self->loop); SD_BUS_DEALLOC_TAIL; } @@ -240,6 +241,13 @@ static PyObject* SdBus_asyncio_update_fd_watchers(SdBusObject* self); #define CHECK_ASYNCIO_WATCHERS ({ CALL_PYTHON_EXPECT_NONE(SdBus_asyncio_update_fd_watchers(self)); }) +static PyObject* _get_or_bind_loop(SdBusObject* self) { + if (NULL == self->loop) { + self->loop = CALL_PYTHON_AND_CHECK(PyObject_CallFunctionObjArgs(asyncio_get_running_loop, NULL)); + } + return self->loop; +} + static PyObject* SdBus_process(SdBusObject* self, PyObject* Py_UNUSED(args)) { int return_value = 1; while (return_value > 0) { @@ -308,7 +316,7 @@ static PyObject* SdBus_call_async(SdBusObject* self, PyObject* args) { SdBusMessageObject* call_message = NULL; CALL_PYTHON_BOOL_CHECK(PyArg_ParseTuple(args, "O", &call_message, NULL)); #endif - PyObject* running_loop CLEANUP_PY_OBJECT = CALL_PYTHON_AND_CHECK(PyObject_CallFunctionObjArgs(asyncio_get_running_loop, 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", "")); @@ -421,7 +429,7 @@ static PyObject* SdBus_match_signal_async(SdBusObject* self, PyObject* args) { CALL_PYTHON_BOOL_CHECK(PyArg_ParseTuple(args, "zzzzO", &sender_service_char_ptr, &path_name_char_ptr, &interface_name_char_ptr, &member_name_char_ptr, &signal_callback, NULL)); #endif - PyObject* running_loop CLEANUP_PY_OBJECT = CALL_PYTHON_AND_CHECK(PyObject_CallFunctionObjArgs(asyncio_get_running_loop, NULL)); + PyObject* running_loop = CALL_PYTHON_AND_CHECK(_get_or_bind_loop(self)); PyObject* new_future CLEANUP_PY_OBJECT = CALL_PYTHON_AND_CHECK(PyObject_CallMethod(running_loop, "create_future", "")); SdBusSlotObject* new_slot CLEANUP_SD_BUS_SLOT = (SdBusSlotObject*)CALL_PYTHON_AND_CHECK(SD_BUS_PY_CLASS_DUNDER_NEW(SdBusSlot_class)); @@ -503,7 +511,7 @@ static PyObject* SdBus_request_name_async(SdBusObject* self, PyObject* args) { CALL_PYTHON_BOOL_CHECK(PyArg_ParseTuple(args, "sK", &service_name_char_ptr, &flags_long_long, NULL)); uint64_t flags = (uint64_t)flags_long_long; #endif - PyObject* running_loop CLEANUP_PY_OBJECT = CALL_PYTHON_AND_CHECK(PyObject_CallFunctionObjArgs(asyncio_get_running_loop, 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", "")); SdBusSlotObject* new_slot_object CLEANUP_SD_BUS_SLOT = (SdBusSlotObject*)CALL_PYTHON_AND_CHECK(SD_BUS_PY_CLASS_DUNDER_NEW(SdBusSlot_class)); @@ -632,7 +640,7 @@ static PyObject* SdBus_asyncio_update_fd_watchers(SdBusObject* self) { self->asyncio_watchers_last_state = events_to_watch; } - PyObject* running_loop CLEANUP_PY_OBJECT = CALL_PYTHON_AND_CHECK(PyObject_CallFunctionObjArgs(asyncio_get_running_loop, NULL)); + PyObject* running_loop = CALL_PYTHON_AND_CHECK(_get_or_bind_loop(self)); PyObject* drive_method CLEANUP_PY_OBJECT = CALL_PYTHON_AND_CHECK(PyObject_GetAttrString((PyObject*)self, "process")); if (NULL == self->bus_fd) { diff --git a/test/test_sdbus_async.py b/test/test_sdbus_async.py index 60f1ff0..1958030 100644 --- a/test/test_sdbus_async.py +++ b/test/test_sdbus_async.py @@ -19,7 +19,9 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA from __future__ import annotations -from asyncio import Event, get_running_loop, sleep, wait_for +from asyncio import Event, get_running_loop +from asyncio import run as asyncio_run +from asyncio import sleep, wait_for from asyncio.subprocess import create_subprocess_exec from typing import TYPE_CHECKING from unittest import SkipTest @@ -975,3 +977,17 @@ async def test_export_handle(self) -> None: with self.assertRaises(DbusUnknownObjectError): await test_object_connection.returns_none_method() + + def test_asyncio_run_different_loops(self) -> None: + bus = self.bus + + async def test() -> None: + dbus_object = DbusInterfaceCommonAsync.new_proxy( + "org.freedesktop.DBus", + "/org/freedesktop/DBus", + bus, + ) + await wait_for(dbus_object.dbus_ping(), timeout=1) + + with self.assertRaisesRegex(RuntimeError, "different loop"): + asyncio_run(test()) From cd658bd1aa9895c9c198f1da952f6d499f93b716 Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sun, 15 Sep 2024 19:37:48 +0100 Subject: [PATCH 113/188] Unregister SdBus file descriptors from event loop on close or deallocation Otherwise it is possible for the event loop to poll on a closed file descriptor which results in undefined behavior. --- src/sdbus/sd_bus_internals_bus.c | 8 ++++++++ test/test_low_level_api.py | 13 ++++++++++++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/src/sdbus/sd_bus_internals_bus.c b/src/sdbus/sd_bus_internals_bus.c index 17394a9..eea0079 100644 --- a/src/sdbus/sd_bus_internals_bus.c +++ b/src/sdbus/sd_bus_internals_bus.c @@ -23,6 +23,10 @@ #include "sd_bus_internals.h" static void SdBus_dealloc(SdBusObject* self) { + if (NULL != self->loop && NULL != self->bus_fd) { + Py_XDECREF(PyObject_CallMethodObjArgs(self->loop, remove_reader_str, self->bus_fd, NULL)); + Py_XDECREF(PyObject_CallMethodObjArgs(self->loop, remove_writer_str, self->bus_fd, NULL)); + } sd_bus_unref(self->sd_bus_ref); Py_XDECREF(self->bus_fd); Py_XDECREF(self->loop); @@ -615,6 +619,10 @@ static PyObject* SdBus_emit_object_removed(SdBusObject* self, PyObject* args) { static PyObject* SdBus_close(SdBusObject* self, PyObject* Py_UNUSED(args)) { sd_bus_close(self->sd_bus_ref); + if (NULL != self->loop && NULL != self->bus_fd) { + Py_XDECREF(CALL_PYTHON_AND_CHECK(PyObject_CallMethodObjArgs(self->loop, remove_reader_str, self->bus_fd, NULL))); + Py_XDECREF(CALL_PYTHON_AND_CHECK(PyObject_CallMethodObjArgs(self->loop, remove_writer_str, self->bus_fd, NULL))); + } Py_RETURN_NONE; } diff --git a/test/test_low_level_api.py b/test/test_low_level_api.py index 1a19a5a..e622ed5 100644 --- a/test/test_low_level_api.py +++ b/test/test_low_level_api.py @@ -19,6 +19,7 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA from __future__ import annotations +from asyncio import get_running_loop from unittest import SkipTest, TestCase, main from sdbus.sd_bus_internals import ( @@ -31,13 +32,23 @@ from sdbus.unittest import IsolatedDbusTestCase -class TestInitDbus(IsolatedDbusTestCase): +class TestAsyncLowLevel(IsolatedDbusTestCase): def test_init_bus(self) -> None: not_connected_bus = SdBus() self.assertIsNone(not_connected_bus.address) self.assertIsNotNone(self.bus.address) + async def test_bus_fd_unregister_close(self) -> None: + await self.bus.request_name_async("org.example", 0) + bus_fd = self.bus.get_fd() + + self.bus.close() + + loop = get_running_loop() + self.assertFalse(loop.remove_reader(bus_fd)) + self.assertFalse(loop.remove_writer(bus_fd)) + class TestLowLeveApi(TestCase): def test_validation_funcs(self) -> None: From fe49edb17582d77c24e8ae9fd9698503db5e2ad1 Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sun, 18 Aug 2024 20:25:07 +0100 Subject: [PATCH 114/188] Use timerfd to process bus timeouts on time sdbus has its own internal timeout system where normally the `sd_bus_get_timeout` function should be used for the maximum time passed to `poll()`. However, Python's asyncio loop does not work with that very well as it has its own internal implementation of polling. Instead create a new timer file descriptor and use it to make asyncio call `SdBus.process()` on given time. This provides a much better performance compared to the `call_later()` implementation as timerfd timeout can be adjusted without allocating any new Python objects. --- src/sdbus/sd_bus_internals.h | 2 ++ src/sdbus/sd_bus_internals_bus.c | 52 ++++++++++++++++++++++++++++++-- test/test_sdbus_async.py | 15 +++++++++ 3 files changed, 66 insertions(+), 3 deletions(-) diff --git a/src/sdbus/sd_bus_internals.h b/src/sdbus/sd_bus_internals.h index b6d7467..71784b3 100644 --- a/src/sdbus/sd_bus_internals.h +++ b/src/sdbus/sd_bus_internals.h @@ -334,7 +334,9 @@ typedef struct { sd_bus* sd_bus_ref; PyObject* bus_fd; PyObject* loop; + PyObject* timer_fd; int asyncio_watchers_last_state; + int timer_fd_int; } SdBusObject; extern PyType_Spec SdBusType; diff --git a/src/sdbus/sd_bus_internals_bus.c b/src/sdbus/sd_bus_internals_bus.c index eea0079..4e9bd0d 100644 --- a/src/sdbus/sd_bus_internals_bus.c +++ b/src/sdbus/sd_bus_internals_bus.c @@ -20,6 +20,8 @@ */ #include #include +#include +#include #include "sd_bus_internals.h" static void SdBus_dealloc(SdBusObject* self) { @@ -27,6 +29,11 @@ static void SdBus_dealloc(SdBusObject* self) { Py_XDECREF(PyObject_CallMethodObjArgs(self->loop, remove_reader_str, self->bus_fd, NULL)); Py_XDECREF(PyObject_CallMethodObjArgs(self->loop, remove_writer_str, self->bus_fd, NULL)); } + if (NULL != self->timer_fd) { + Py_XDECREF(PyObject_CallMethodObjArgs(self->loop, remove_reader_str, self->bus_fd, NULL)); + Py_DECREF(self->timer_fd); + close(self->timer_fd_int); + } sd_bus_unref(self->sd_bus_ref); Py_XDECREF(self->bus_fd); Py_XDECREF(self->loop); @@ -623,6 +630,10 @@ static PyObject* SdBus_close(SdBusObject* self, PyObject* Py_UNUSED(args)) { Py_XDECREF(CALL_PYTHON_AND_CHECK(PyObject_CallMethodObjArgs(self->loop, remove_reader_str, self->bus_fd, NULL))); Py_XDECREF(CALL_PYTHON_AND_CHECK(PyObject_CallMethodObjArgs(self->loop, remove_writer_str, self->bus_fd, NULL))); } + if (NULL != self->timer_fd) { + Py_XDECREF(PyObject_CallMethodObjArgs(self->loop, remove_reader_str, self->bus_fd, NULL)); + // TODO: Close timerfd + } Py_RETURN_NONE; } @@ -639,7 +650,45 @@ static inline int sd_bus_get_events_zero_on_closed(SdBusObject* self) { return events; }; +static inline int sd_bus_get_timeout_uint_max_on_closed(SdBusObject* self, uint64_t* timeout_usec) { + int r = sd_bus_get_timeout(self->sd_bus_ref, timeout_usec); + if (-ENOTCONN == r) { + *timeout_usec = UINT64_MAX; + return 0; + } + return r; +} + static PyObject* SdBus_asyncio_update_fd_watchers(SdBusObject* self) { + PyObject* running_loop = CALL_PYTHON_AND_CHECK(_get_or_bind_loop(self)); + PyObject* drive_method CLEANUP_PY_OBJECT = CALL_PYTHON_AND_CHECK(PyObject_GetAttrString((PyObject*)self, "process")); + + if (NULL == self->timer_fd) { + self->timer_fd_int = CALL_SD_BUS_AND_CHECK(timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK | TFD_CLOEXEC)); + if (self->timer_fd_int < 0) { + PyErr_SetFromErrno(PyExc_OSError); + } + PyObject* timer_fd CLEANUP_PY_OBJECT = PyLong_FromLong((int)self->timer_fd_int); + Py_XDECREF(CALL_PYTHON_AND_CHECK(PyObject_CallMethodObjArgs(running_loop, add_reader_str, timer_fd, drive_method, NULL))); + Py_INCREF(timer_fd); + self->timer_fd = timer_fd; + } + + uint64_t timeout_usec = UINT64_MAX; + CALL_SD_BUS_AND_CHECK(sd_bus_get_timeout_uint_max_on_closed(self, &timeout_usec)); + + struct itimerspec bus_timer = {0}; + if (timeout_usec == UINT64_MAX) { + // Setting bus_timer to zero disarms timer. + } else if (timeout_usec != 0) { + bus_timer.it_value.tv_sec = timeout_usec / 1000000; + bus_timer.it_value.tv_nsec = (timeout_usec % 1000000) * 1000; + } else if (timeout_usec == 0) { + Py_XDECREF(CALL_PYTHON_AND_CHECK(PyObject_CallMethodObjArgs(running_loop, call_soon_str, drive_method, NULL))); + } + + CALL_SD_BUS_AND_CHECK(timerfd_settime(self->timer_fd_int, TFD_TIMER_ABSTIME, &bus_timer, NULL)); + int events_to_watch = CALL_SD_BUS_AND_CHECK(sd_bus_get_events_zero_on_closed(self)); if (events_to_watch == self->asyncio_watchers_last_state) { // Do not update the watchers because state is the same @@ -648,9 +697,6 @@ static PyObject* SdBus_asyncio_update_fd_watchers(SdBusObject* self) { self->asyncio_watchers_last_state = events_to_watch; } - PyObject* running_loop = CALL_PYTHON_AND_CHECK(_get_or_bind_loop(self)); - PyObject* drive_method CLEANUP_PY_OBJECT = CALL_PYTHON_AND_CHECK(PyObject_GetAttrString((PyObject*)self, "process")); - if (NULL == self->bus_fd) { self->bus_fd = CALL_PYTHON_AND_CHECK(SdBus_get_fd(self, NULL)); } diff --git a/test/test_sdbus_async.py b/test/test_sdbus_async.py index 1958030..a328afa 100644 --- a/test/test_sdbus_async.py +++ b/test/test_sdbus_async.py @@ -29,6 +29,7 @@ from sdbus.exceptions import ( DbusFailedError, DbusFileExistsError, + DbusNoReplyError, DbusPropertyReadOnlyError, DbusUnknownObjectError, SdBusLibraryError, @@ -721,6 +722,20 @@ async def too_long_wait() -> None: with self.assertRaises(SdBusLibraryError): await wait_for(too_long_wait(), timeout=1) + async def test_bus_timerfd(self) -> None: + test_object, test_object_connection = initialize_object() + + self.bus.method_call_timeout_usec = 10_000 # 0.01 seconds + + loop = get_running_loop() + + start = loop.time() + + with self.assertRaises(DbusNoReplyError): + await wait_for(test_object_connection.looong_method(), timeout=1) + + self.assertAlmostEqual(loop.time() - start, 0.01, delta=0.01) + async def test_signal_queue_wildcard_match(self) -> None: test_object, test_object_connection = initialize_object() From 266fca55fec67e4b135e8704118ee2db24cf004a Mon Sep 17 00:00:00 2001 From: igo95862 Date: Mon, 23 Sep 2024 21:02:33 +0100 Subject: [PATCH 115/188] Fix timerfd reader not being removed from asyncio loop The copy pasted code in the timer fd paths removed the bus fd. Thanks @ofacklam for noticing it. --- src/sdbus/sd_bus_internals_bus.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sdbus/sd_bus_internals_bus.c b/src/sdbus/sd_bus_internals_bus.c index 4e9bd0d..f66a14d 100644 --- a/src/sdbus/sd_bus_internals_bus.c +++ b/src/sdbus/sd_bus_internals_bus.c @@ -30,7 +30,7 @@ static void SdBus_dealloc(SdBusObject* self) { Py_XDECREF(PyObject_CallMethodObjArgs(self->loop, remove_writer_str, self->bus_fd, NULL)); } if (NULL != self->timer_fd) { - Py_XDECREF(PyObject_CallMethodObjArgs(self->loop, remove_reader_str, self->bus_fd, NULL)); + Py_XDECREF(PyObject_CallMethodObjArgs(self->loop, remove_reader_str, self->timer_fd, NULL)); Py_DECREF(self->timer_fd); close(self->timer_fd_int); } @@ -631,7 +631,7 @@ static PyObject* SdBus_close(SdBusObject* self, PyObject* Py_UNUSED(args)) { Py_XDECREF(CALL_PYTHON_AND_CHECK(PyObject_CallMethodObjArgs(self->loop, remove_writer_str, self->bus_fd, NULL))); } if (NULL != self->timer_fd) { - Py_XDECREF(PyObject_CallMethodObjArgs(self->loop, remove_reader_str, self->bus_fd, NULL)); + Py_XDECREF(PyObject_CallMethodObjArgs(self->loop, remove_reader_str, self->timer_fd, NULL)); // TODO: Close timerfd } Py_RETURN_NONE; From 87aefaabea3c58dd28adfb2a6be3e9685e70649f Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sun, 2 Jun 2024 21:30:20 +0500 Subject: [PATCH 116/188] Split sdbus.utils package Move current parsing utilities to `sdbus.utils.parse` subpackage. For backwards compatibility import them to the `__init__.py`. This allows better package separation in the future. --- docs/asyncio_api.rst | 8 +++--- docs/utils.rst | 5 +++- setup.py | 10 +++++--- src/sdbus/utils/__init__.py | 34 ++++++++++++++++++++++++++ src/sdbus/{utils.py => utils/parse.py} | 6 ++--- test/test_sdbus_async.py | 2 +- 6 files changed, 52 insertions(+), 13 deletions(-) create mode 100644 src/sdbus/utils/__init__.py rename src/sdbus/{utils.py => utils/parse.py} (97%) diff --git a/docs/asyncio_api.rst b/docs/asyncio_api.rst index c94cedc..ccffeee 100644 --- a/docs/asyncio_api.rst +++ b/docs/asyncio_api.rst @@ -75,7 +75,7 @@ Classes Signal when one of the objects properties changes. - :py:func:`sdbus.utils.parse_properties_changed` can be used to transform + :py:func:`sdbus.utils.parse.parse_properties_changed` can be used to transform this signal data in to an easier to work with dictionary. Signal data is: @@ -192,7 +192,7 @@ Classes Get the objects this object manager in managing. - :py:func:`sdbus.utils.parse_get_managed_objects` can be used + :py:func:`sdbus.utils.parse.parse_get_managed_objects` can be used to make returned data easier to work with. :return: @@ -209,7 +209,7 @@ Classes Signal when a new object is added or and existing object gains a new interface. - :py:func:`sdbus.utils.parse_interfaces_added` can be used + :py:func:`sdbus.utils.parse.parse_interfaces_added` can be used to make signal data easier to work with. Signal data is: @@ -226,7 +226,7 @@ Classes Signal when existing object or and interface of existing object is removed. - :py:func:`sdbus.utils.parse_interfaces_removed` can be used + :py:func:`sdbus.utils.parse.parse_interfaces_removed` can be used to make signal data easier to work with. Signal data is: diff --git a/docs/utils.rst b/docs/utils.rst index 9dcf840..10d982e 100644 --- a/docs/utils.rst +++ b/docs/utils.rst @@ -4,7 +4,10 @@ Utilities Parsing utilities +++++++++++++++++ -.. py:currentmodule:: sdbus.utils +Parse unweildy D-Bus structures in to Python native objects and names. +Available under ``sdbus.utils.parse`` subpackage. + +.. py:currentmodule:: sdbus.utils.parse .. py:function:: parse_properties_changed(interface, properties_changed_data, on_unknown_member='error') diff --git a/setup.py b/setup.py index f11dd69..0830ac1 100644 --- a/setup.py +++ b/setup.py @@ -118,10 +118,12 @@ def get_link_arguments() -> List[str]: 'Programming Language :: Python :: 3 :: Only', 'Topic :: Software Development :: Libraries :: Python Modules', ], - packages=['sdbus', - # 'sdbus_async', 'sdbus_block', - 'sdbus_async.dbus_daemon', 'sdbus_block.dbus_daemon', - ], + packages=[ + 'sdbus', + 'sdbus.utils', + 'sdbus_async.dbus_daemon', + 'sdbus_block.dbus_daemon', + ], package_dir={ 'sdbus': 'src/sdbus', 'sdbus_async.dbus_daemon': 'src/sdbus_async/dbus_daemon', diff --git a/src/sdbus/utils/__init__.py b/src/sdbus/utils/__init__.py new file mode 100644 index 0000000..11fa044 --- /dev/null +++ b/src/sdbus/utils/__init__.py @@ -0,0 +1,34 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later + +# Copyright (C) 2024 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 .parse import ( + parse_get_managed_objects, + parse_interfaces_added, + parse_interfaces_removed, + parse_properties_changed, +) + +__all__ = ( + "parse_get_managed_objects", + "parse_interfaces_added", + "parse_interfaces_removed", + "parse_properties_changed", +) diff --git a/src/sdbus/utils.py b/src/sdbus/utils/parse.py similarity index 97% rename from src/sdbus/utils.py rename to src/sdbus/utils/parse.py index 7c666a3..f2b749d 100644 --- a/src/sdbus/utils.py +++ b/src/sdbus/utils/parse.py @@ -21,8 +21,8 @@ from typing import TYPE_CHECKING -from .dbus_common_funcs import _parse_properties_vardict -from .dbus_proxy_async_interface_base import ( +from ..dbus_common_funcs import _parse_properties_vardict +from ..dbus_proxy_async_interface_base import ( DBUS_CLASS_TO_META, DBUS_INTERFACE_NAME_TO_CLASS, DbusInterfaceBaseAsync, @@ -42,7 +42,7 @@ Union, ) - from .dbus_proxy_async_interfaces import DBUS_PROPERTIES_CHANGED_TYPING + from ..dbus_proxy_async_interfaces import DBUS_PROPERTIES_CHANGED_TYPING InterfacesInputElements = Union[ DbusInterfaceBaseAsync, diff --git a/test/test_sdbus_async.py b/test/test_sdbus_async.py index a328afa..a2d4dc7 100644 --- a/test/test_sdbus_async.py +++ b/test/test_sdbus_async.py @@ -40,7 +40,7 @@ DbusPropertyEmitsChangeFlag, ) from sdbus.unittest import IsolatedDbusTestCase -from sdbus.utils import parse_properties_changed +from sdbus.utils.parse import parse_properties_changed from sdbus import ( DbusInterfaceCommonAsync, From edf2404a720e2238682f4937781f2b36988eba6d Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sun, 2 Jun 2024 23:10:57 +0500 Subject: [PATCH 117/188] Add sdbus.utils.inspect.inspect_dbus_path If called on a D-Bus proxy returns path of the proxied object. If called on a local D-Bus object returns the exported D-Bus path. If object is not exported raises ``LookupError``. If called on an object that is unrelated to D-Bus raises ``TypeError``. The object's path is inspected in the context of the given bus and if the object is attached to a different bus the ``LookupError`` will be raised. If the bus argument is not given or is ``None`` the default bus will be checked against. --- docs/utils.rst | 34 +++++++++++++++ src/sdbus/utils/inspect.py | 86 ++++++++++++++++++++++++++++++++++++++ test/test_sdbus_utils.py | 70 +++++++++++++++++++++++++++++++ 3 files changed, 190 insertions(+) create mode 100644 src/sdbus/utils/inspect.py create mode 100644 test/test_sdbus_utils.py diff --git a/docs/utils.rst b/docs/utils.rst index 10d982e..9644c89 100644 --- a/docs/utils.rst +++ b/docs/utils.rst @@ -84,3 +84,37 @@ Available under ``sdbus.utils.parse`` subpackage. :returns: Dictionary where keys are paths and values are tuples of managed objects classes and their properties data. *New in version 0.12.0.* + +Inspect utilities ++++++++++++++++++ + +Inspect D-Bus objects and retrieve their D-Bus related attributes +such as D-Bus object paths and etc... +Available under ``sdbus.utils.inspect`` subpackage. + +.. py:currentmodule:: sdbus.utils.inspect + +.. py:function:: inspect_dbus_path(obj, bus=None) + + Returns the D-Bus path of an object. + + If called on a D-Bus proxy returns path of the proxied object. + + If called on a local D-Bus object returns the exported D-Bus path. + If object is not exported raises ``LookupError``. + + If called on an object that is unrelated to D-Bus raises ``TypeError``. + + The object's path is inspected in the context of the given bus and if the + object is attached to a different bus the ``LookupError`` will be raised. + If the bus argument is not given or is ``None`` the default bus will be + checked against. + + :param object obj: Object to inspect. + :param SdBus bus: + Bus to inspect against. + If not given or ``None`` the default bus will be used. + :rtype: str + :returns: D-Bus path of the object. + + *New in version 0.13.0.* diff --git a/src/sdbus/utils/inspect.py b/src/sdbus/utils/inspect.py new file mode 100644 index 0000000..f34f550 --- /dev/null +++ b/src/sdbus/utils/inspect.py @@ -0,0 +1,86 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later + +# Copyright (C) 2024 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 typing import TYPE_CHECKING + +from ..dbus_common_elements import DbusLocalObjectMeta, DbusRemoteObjectMeta +from ..dbus_common_funcs import get_default_bus +from ..dbus_proxy_async_interface_base import DbusInterfaceBaseAsync +from ..dbus_proxy_sync_interface_base import DbusInterfaceBase + +if TYPE_CHECKING: + from typing import Optional, Union + + from ..sd_bus_internals import SdBus + + +def _inspect_dbus_path_proxy( + obj: object, + dbus_meta: DbusRemoteObjectMeta, + bus: SdBus, +) -> str: + if bus != dbus_meta.attached_bus: + raise LookupError( + f"D-Bus proxy {obj!r} at {dbus_meta.object_path!r} path " + f"is not attached to bus {bus!r}" + ) + + return dbus_meta.object_path + + +def _inspect_dbus_path_local( + obj: object, + dbus_meta: DbusLocalObjectMeta, + bus: SdBus, +) -> str: + attached_bus = dbus_meta.attached_bus + object_path = dbus_meta.serving_object_path + if attached_bus is None or object_path is None: + raise LookupError( + f"Local D-Bus object {obj!r} is not exported to any D-Bus" + ) + + if bus != attached_bus: + raise LookupError( + f"Local D-Bus object {obj!r} at {dbus_meta.serving_object_path!r} " + f"path is not attached to bus {bus!r}" + ) + + return object_path + + +def inspect_dbus_path( + obj: Union[DbusInterfaceBase, DbusInterfaceBaseAsync], + bus: Optional[SdBus] = None, +) -> str: + if bus is None: + bus = get_default_bus() + + if isinstance(obj, DbusInterfaceBase): + return _inspect_dbus_path_proxy(obj, obj._dbus, bus) + elif isinstance(obj, DbusInterfaceBaseAsync): + dbus_meta = obj._dbus + if isinstance(dbus_meta, DbusRemoteObjectMeta): + return _inspect_dbus_path_proxy(obj, dbus_meta, bus) + else: + return _inspect_dbus_path_local(obj, dbus_meta, bus) + else: + raise TypeError(f"Expected D-Bus object got {obj!r}") diff --git a/test/test_sdbus_utils.py b/test/test_sdbus_utils.py new file mode 100644 index 0000000..18b3050 --- /dev/null +++ b/test/test_sdbus_utils.py @@ -0,0 +1,70 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later + +# Copyright (C) 2024 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 sdbus.unittest import IsolatedDbusTestCase +from sdbus.utils.inspect import inspect_dbus_path + +from sdbus import ( + DbusInterfaceCommon, + DbusInterfaceCommonAsync, + sd_bus_open_user, +) + +TEST_PATH = "/test" + + +class TestSdbusUtilsInspect(IsolatedDbusTestCase): + def test_inspect_dbus_path_block(self) -> None: + proxy = DbusInterfaceCommon("example.org", TEST_PATH) + + self.assertEqual(inspect_dbus_path(proxy), TEST_PATH) + + new_bus = sd_bus_open_user() + + with self.assertRaisesRegex(LookupError, "is not attached to bus"): + inspect_dbus_path(proxy, new_bus) + + def test_inspect_dbus_path_async_proxy(self) -> None: + proxy = DbusInterfaceCommonAsync.new_proxy("example.org", TEST_PATH) + + self.assertEqual(inspect_dbus_path(proxy), TEST_PATH) + + new_bus = sd_bus_open_user() + + with self.assertRaisesRegex(LookupError, "is not attached to bus"): + inspect_dbus_path(proxy, new_bus) + + def test_inspect_dbus_path_async_local(self) -> None: + local_obj = DbusInterfaceCommonAsync() + + with self.assertRaisesRegex( + LookupError, "is not exported to any D-Bus", + ): + inspect_dbus_path(local_obj) + + local_obj.export_to_dbus(TEST_PATH) + + self.assertEqual(inspect_dbus_path(local_obj), TEST_PATH) + + new_bus = sd_bus_open_user() + + with self.assertRaisesRegex(LookupError, "is not attached to bus"): + inspect_dbus_path(local_obj, new_bus) From 34dae7943cd234dd12f89f2f83b44962e5be651a Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sun, 13 Oct 2024 19:52:42 +0100 Subject: [PATCH 118/188] Set __main__ subcommands as required Instead of cryptic KeyError a message about the required arguments will be set. Also set the `prog=` argument of the ArgumentParser to `sdbus` so that it would be shown in help instead of `__main__.py`. --- src/sdbus/__main__.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/sdbus/__main__.py b/src/sdbus/__main__.py index b1cb494..b81cdbd 100644 --- a/src/sdbus/__main__.py +++ b/src/sdbus/__main__.py @@ -89,8 +89,13 @@ def run_gen_from_file( def generator_main(args: Optional[List[str]] = None) -> None: - main_arg_parser = ArgumentParser() - subparsers = main_arg_parser.add_subparsers() + main_arg_parser = ArgumentParser( + prog="sdbus", + ) + subparsers = main_arg_parser.add_subparsers( + required=True, + title="subcommands", + ) generate_from_file_parser = subparsers.add_parser('gen-from-file') generate_from_file_parser.set_defaults(func=run_gen_from_file) From 9f1d48d7df85c42c3407afb67c408df007ad57e9 Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sun, 13 Oct 2024 20:07:31 +0100 Subject: [PATCH 119/188] Fix CI by using venv Instead of installing using pip in to the system folders use a venv for an isolated environment. --- .github/workflows/ubuntu_test.yml | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ubuntu_test.yml b/.github/workflows/ubuntu_test.yml index ac4d2a5..f9cadab 100644 --- a/.github/workflows/ubuntu_test.yml +++ b/.github/workflows/ubuntu_test.yml @@ -51,13 +51,16 @@ jobs: - name: Install dependencies run: | sudo apt update - sudo apt install python3 python3-pip meson - sudo pip3 install --upgrade mypy isort flake8 pyflakes pycodestyle \ - jinja2 Sphinx types-setuptools + sudo apt install python3 python3-pip ninja-build + python -m venv --system-site-packages venv + ./venv/bin/pip install --upgrade \ + mypy isort flake8 pyflakes pycodestyle \ + jinja2 Sphinx types-setuptools meson - name: Run linters run: | + export PATH="$(readlink -f ./venv/bin):${PATH}" meson setup build - ninja -C build lint-python + meson compile -C build lint-python alpine: name: Alpine Linux test runs-on: ubuntu-latest From 39fd0f320b5de5a51833f3f0ddb6961df7a09329 Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sat, 19 Oct 2024 18:21:38 +0100 Subject: [PATCH 120/188] Rework interface_generator to use trim_blocks and lstrip_blocks This makes the template strings easier to understand as each filter or include can be indented without affecting the newlines. Also add a note to documentation that style is not guranteed by the code generator. Only syntax will be checked. Add unit test to check that syntax is correct. --- docs/code_generator.rst | 3 + src/sdbus/interface_generator.py | 223 ++++++++++++++++++------------- test/test_interface_generator.py | 71 ++++++++++ 3 files changed, 201 insertions(+), 96 deletions(-) diff --git a/docs/code_generator.rst b/docs/code_generator.rst index 7f18f2e..955debf 100644 --- a/docs/code_generator.rst +++ b/docs/code_generator.rst @@ -13,6 +13,9 @@ to be installed. .. warning:: Do NOT send the generator result to ``exec()`` function. Interface code MUST be inspected before running. +The generated interfaces code will be syntactically correct but NOT stylistically. +It is recommended running a code formatter on the generated code. (for example ``black``) + Generating from XML files ------------------------- diff --git a/src/sdbus/interface_generator.py b/src/sdbus/interface_generator.py index 3d7a4a3..33e6866 100644 --- a/src/sdbus/interface_generator.py +++ b/src/sdbus/interface_generator.py @@ -555,38 +555,41 @@ def has_members(self) -> bool: 'org.freedesktop.DBus.ObjectManager', } + INTERFACE_TEMPLATES: Dict[str, str] = { - "generic_no_members": r"... # Interface has no members", + "generic_no_members": """\ +... # Interface has no members +""", "generic_method_flags": ( - r""" -{%- if method.dbus_input_signature %} + """\ +{% if method.dbus_input_signature %} input_signature="{{ method.dbus_input_signature }}", -{%- endif %} - -{%- if method.dbus_result_signature %} +{% endif %} +{% if method.dbus_result_signature %} result_signature="{{ method.dbus_result_signature }}", -{%- endif %} - -{%- if method.flags_str %} +{% endif %} +{% if method.flags_str %} flags={{ method.flags_str }}, -{%- endif %} +{% endif %} """ ), "generic_property_flags": ( - r""" -{%- if a_property.dbus_signature %} + """\ +{% if a_property.dbus_signature %} property_signature="{{ a_property.dbus_signature }}", -{%- endif %} - -{%- if a_property.flags_str %} +{% endif %} +{% if a_property.flags_str %} flags={{ a_property.flags_str }}, -{%- endif %} +{% endif %} """ ), - "generic_header": r"""from __future__ import annotations + "generic_header": """\ +from __future__ import annotations -from typing import Any, Dict, List, Tuple""", - "async_imports_header": r"""from sdbus import ( +from typing import Any, Dict, List, Tuple + +""", + "async_imports_header": """from sdbus import ( DbusDeprecatedFlag, DbusInterfaceCommonAsync, DbusNoReplyFlag, @@ -598,84 +601,96 @@ def has_members(self) -> bool: dbus_method_async, dbus_property_async, dbus_signal_async, -)""", +) + +""", "async_main": ( - r"""{% if include_import_header -%} -{% include 'generic_header' %} + """\ +{% if include_import_header %} + {% include 'generic_header' %} + + {% include 'async_imports_header' %} +{% endif %} -{% include 'async_imports_header' %} -{%- endif %} {% for interface in interfaces %} -{% include 'async_interface' %} -{%- endfor %} + {% include 'async_interface' %} +{% endfor %} """ ), "async_interface": ( - r"""class {{ interface.python_name }}( + """\ +class {{ interface.python_name }}( DbusInterfaceCommonAsync, interface_name="{{ interface.interface_name }}", ): -{%- filter indent -%} -{%- if interface.has_members -%} -{% for method in interface.methods -%} -{% include 'async_method' %} -{% endfor -%} -{% for a_property in interface.properties -%} -{% include 'async_property' %} -{% endfor -%} -{% for signal in interface.signals -%} -{% include 'async_signal' %} -{% endfor -%} -{%- else %} -{% include 'generic_no_members' %} -{% endif -%} -{%- endfilter -%} +{% filter indent(first=True) %} + {% if interface.has_members %} + {% for method in interface.methods %} + {% include 'async_method' %} + + {% endfor %} + {% for a_property in interface.properties %} + {% include 'async_property' %} + + {% endfor %} + {% for signal in interface.signals %} + {% include 'async_signal' %} + + {% endfor %} + {% else %} + {% include 'generic_no_members' %} + + {% endif %} +{% endfilter %} """ ), "async_method": ( - r""" + """\ @dbus_method_async( -{%- filter indent -%} -{%- include 'generic_method_flags' -%} -{%- endfilter %} +{% filter indent(first=True) %} + {% include 'generic_method_flags' %} +{% endfilter %} ) async def {{ method.python_name }}( self, - -{%- for arg_name, arg_type in method.args_names_and_typing %} +{% for arg_name, arg_type in method.args_names_and_typing %} {{ arg_name }}: {{ arg_type }}, -{%- endfor %} +{% endfor %} ) -> {{ method.result_typing }}: raise NotImplementedError + """ ), "async_property": ( - r""" + """\ @dbus_property_async( -{%- filter indent -%} -{%- include 'generic_property_flags' -%} -{%- endfilter %} +{% filter indent(first=True) %} + {% include 'generic_property_flags' %} +{% endfilter %} ) def {{ a_property.python_name }}(self) -> {{ a_property.typing }}: - raise NotImplementedError""" + raise NotImplementedError + +""" ), "async_signal": ( - r""" + """\ @dbus_signal_async( - -{%- if signal.dbus_signature %} +{% if signal.dbus_signature %} signal_signature="{{ signal.dbus_signature }}", -{%- endif %} - -{%- if signal.flags_str %} +{% endif %} +{% if signal.flags_str %} flags={{ signal.flags_str }}, -{%- endif %} +{% endif %} ) def {{ signal.python_name }}(self) -> {{ signal.typing }}: - raise NotImplementedError""" + raise NotImplementedError + +""" ), - "blocking_imports_header": r"""from sdbus import ( + "blocking_imports_header": """\ +from sdbus import ( DbusDeprecatedFlag, DbusInterfaceCommon, DbusNoReplyFlag, @@ -686,64 +701,75 @@ def {{ signal.python_name }}(self) -> {{ signal.typing }}: DbusUnprivilegedFlag, dbus_method, dbus_property, -)""", +) + +""", "blocking_main": ( - r"""{% if include_import_header -%} -{% include 'generic_header' %} + """\ +{% if include_import_header %} + {% include 'generic_header' %} + + {% include 'blocking_imports_header' %} +{% endif %} -{% include 'blocking_imports_header' %} -{%- endif %} {% for interface in interfaces %} -{% include 'blocking_interface' %} -{%- endfor %} + {% include 'blocking_interface' %} +{% endfor %} + """ ), "blocking_interface": ( - r"""class {{ interface.python_name }}( + """\ +class {{ interface.python_name }}( DbusInterfaceCommon, interface_name="{{ interface.interface_name }}", ): -{%- filter indent -%} -{%- if interface.has_members -%} -{% for method in interface.methods -%} -{% include 'blocking_method' %} -{% endfor -%} -{% for a_property in interface.properties -%} -{% include 'blocking_property' %} -{% endfor -%} -{%- else %} -{% include 'generic_no_members' %} -{% endif -%} -{%- endfilter -%} +{% filter indent(first=True) %} + {% if interface.has_members %} + {% for method in interface.methods %} + {% include 'blocking_method' %} + + {% endfor %} + {% for a_property in interface.properties %} + {% include 'blocking_property' %} + + {% endfor %} + {% else %} + {% include 'generic_no_members' %} + + {% endif %} +{% endfilter %} """ ), "blocking_method": ( - r""" + """\ @dbus_method( -{%- filter indent -%} -{%- include 'generic_method_flags' -%} -{%- endfilter %} +{% filter indent(first=True) %} + {% include 'generic_method_flags' %} +{% endfilter %} ) def {{ method.python_name }}( self, - -{%- for arg_name, arg_type in method.args_names_and_typing %} +{% for arg_name, arg_type in method.args_names_and_typing %} {{ arg_name }}: {{ arg_type }}, -{%- endfor %} +{% endfor %} ) -> {{ method.result_typing }}: raise NotImplementedError + """ ), "blocking_property": ( - r""" + """\ @dbus_property( -{%- filter indent -%} -{%- include 'generic_property_flags' -%} -{%- endfilter %} +{% filter indent(first=True) %} + {% include 'generic_property_flags' %} +{% endfilter %} ) def {{ a_property.python_name }}(self) -> {{ a_property.typing }}: - raise NotImplementedError""" + raise NotImplementedError + +""" ), } @@ -795,7 +821,12 @@ def generate_py_file( template_name = "async_main" if do_async else "blocking_main" - env = JinjaEnv(loader=DictLoader(INTERFACE_TEMPLATES)) + env = JinjaEnv( + loader=DictLoader(INTERFACE_TEMPLATES), + trim_blocks=True, + lstrip_blocks=True, + autoescape=False, + ) return env.get_template(template_name).render( interfaces=interfaces, include_import_header=include_import_header, diff --git a/test/test_interface_generator.py b/test/test_interface_generator.py index 1cfb075..aca9792 100644 --- a/test/test_interface_generator.py +++ b/test/test_interface_generator.py @@ -262,5 +262,76 @@ def test_generate_from_connection_blocking(self) -> None: ) +INTERFACE_NO_MEMBERS_XML = """ + + + + + + + +""" + + +class TestGeneratorSyntaxCompile(TestCase): + def test_syntax_compile_async(self) -> None: + source_code = generate_py_file( + interfaces_from_str(test_xml), + do_async=True, + ) + compile(source_code, filename="", mode="exec") + + def test_syntax_compile_block(self) -> None: + source_code = generate_py_file( + interfaces_from_str(test_xml), + do_async=False, + ) + compile(source_code, filename="", mode="exec") + + def test_syntax_no_members_interface(self) -> None: + + regular_interface = interfaces_from_str(test_xml) + no_members_interface = interfaces_from_str(INTERFACE_NO_MEMBERS_XML) + + self.assertFalse(no_members_interface[0].methods) + self.assertFalse(no_members_interface[0].properties) + self.assertFalse(no_members_interface[0].signals) + + compile( + generate_py_file( + regular_interface + no_members_interface, + do_async=True, + ), + filename="", + mode="exec", + ) + compile( + generate_py_file( + no_members_interface + regular_interface, + do_async=True, + ), + filename="", + mode="exec", + ) + + compile( + generate_py_file( + regular_interface + no_members_interface, + do_async=False, + ), + filename="", + mode="exec", + ) + compile( + generate_py_file( + no_members_interface + regular_interface, + do_async=False, + ), + filename="", + mode="exec", + ) + + if __name__ == "__main__": main() From 45943abcdc2fef3baaf0bf1ffbc0e31638dd2503 Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sat, 19 Oct 2024 18:43:11 +0100 Subject: [PATCH 121/188] ci: Install Sphinx < 8.0 Version 8.0 dropped support for Python versions sdbus still supports. This results in mypy failures. --- .github/workflows/ubuntu_test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ubuntu_test.yml b/.github/workflows/ubuntu_test.yml index f9cadab..6c9b019 100644 --- a/.github/workflows/ubuntu_test.yml +++ b/.github/workflows/ubuntu_test.yml @@ -55,7 +55,7 @@ jobs: python -m venv --system-site-packages venv ./venv/bin/pip install --upgrade \ mypy isort flake8 pyflakes pycodestyle \ - jinja2 Sphinx types-setuptools meson + jinja2 'Sphinx<8.0' types-setuptools meson - name: Run linters run: | export PATH="$(readlink -f ./venv/bin):${PATH}" From 1ebb8c25686c71aff1dfd9cda6069892bffa9689 Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sat, 19 Oct 2024 18:51:09 +0100 Subject: [PATCH 122/188] docs: Update Jinja link It has a new home page. Old link no longer works. --- docs/code_generator.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/code_generator.rst b/docs/code_generator.rst index 955debf..d1978f7 100644 --- a/docs/code_generator.rst +++ b/docs/code_generator.rst @@ -7,7 +7,7 @@ Currently async interfaces code is generated by default. Blocking interfaces can be generated by passing ``--block`` option. Running code generator requires -`Jinja2 `_ +`Jinja `_ to be installed. .. warning:: Do NOT send the generator result to ``exec()`` function. From 01a0e44b2fdd0f2f292ead6125c254347d73cbd9 Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sat, 19 Oct 2024 19:13:20 +0100 Subject: [PATCH 123/188] Fix Privileged flag being inverted by interface generator By default libsystemd makes all methods privileged unless the `org.freedesktop.systemd1.Privileged` is set. The code generator only set the unprivileged flag only if introspection flag was set which is completely inverted logic. This change means there will be a lot of `DbusUnprivilegedFlag` used in the generated code but this is a correct way. --- src/sdbus/interface_generator.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/sdbus/interface_generator.py b/src/sdbus/interface_generator.py index 33e6866..f4b469b 100644 --- a/src/sdbus/interface_generator.py +++ b/src/sdbus/interface_generator.py @@ -273,7 +273,7 @@ def __init__(self, element: Element): self.python_name = camel_case_to_snake_case(self.method_name) self.is_deprecated = False - self.is_unpriveledged = False + self.is_priveledged = False self.iter_sub_elements(element) @@ -281,7 +281,7 @@ def _flags_iter(self) -> Iterator[str]: if self.is_deprecated: yield 'DbusDeprecatedFlag' - if self.is_unpriveledged: + if not self.is_priveledged: yield 'DbusUnprivilegedFlag' @property @@ -298,7 +298,7 @@ def _parse_annotation_data(self, if annotation_name == 'org.freedesktop.DBus.Deprecated': self.is_deprecated = parse_str_bool(annotation_value) elif annotation_name == 'org.freedesktop.systemd1.Privileged': - self.is_unpriveledged = parse_str_bool(annotation_value) + self.is_priveledged = parse_str_bool(annotation_value) else: ... From 7d7629169411c3837b38201cc2646b93092200c6 Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sat, 26 Oct 2024 20:00:59 +0100 Subject: [PATCH 124/188] Add result arg names to generated interfaces method and signals Only add them if all arguments have names because the names are actually optional. --- src/sdbus/interface_generator.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/sdbus/interface_generator.py b/src/sdbus/interface_generator.py index f4b469b..0c5930a 100644 --- a/src/sdbus/interface_generator.py +++ b/src/sdbus/interface_generator.py @@ -414,6 +414,14 @@ def result_typing(self) -> str: return DbusSigToTyping.result_typing( [x.dbus_type for x in self.result_args]) + @property + def is_results_args_valid_names(self) -> bool: + return all((r.name is not None for r in self.result_args)) + + @property + def result_args_names_repr(self) -> str: + return repr(tuple(r.name for r in self.result_args)) + def __repr__(self) -> str: return (f"D-Bus Method: {self.method_name}, " f"args: {self.args_names_and_typing}, " @@ -507,6 +515,14 @@ def typing(self) -> str: return DbusSigToTyping.result_typing( [x.dbus_type for x in self.args]) + @property + def is_args_valid_names(self) -> bool: + return all((a.name is not None for a in self.args)) + + @property + def args_names_repr(self) -> str: + return repr(tuple(a.name for a in self.args)) + class DbusInterfaceIntrospection: def __init__(self, element: Element): @@ -568,6 +584,9 @@ def has_members(self) -> bool: {% if method.dbus_result_signature %} result_signature="{{ method.dbus_result_signature }}", {% endif %} +{% if method.is_results_args_valid_names %} +result_args_names={{method.result_args_names_repr}}, +{% endif %} {% if method.flags_str %} flags={{ method.flags_str }}, {% endif %} @@ -680,6 +699,9 @@ def {{ a_property.python_name }}(self) -> {{ a_property.typing }}: {% if signal.dbus_signature %} signal_signature="{{ signal.dbus_signature }}", {% endif %} +{% if signal.is_args_valid_names %} + signal_args_names={{signal.args_names_repr}}, +{% endif %} {% if signal.flags_str %} flags={{ signal.flags_str }}, {% endif %} From f08081bf491beb56ff21620f792183b15300b044 Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sat, 26 Oct 2024 20:55:18 +0100 Subject: [PATCH 125/188] Only add unprivileged flag to where it can be used in generated code Apparently signals and non-writeable properties cannot be unprivileged. Otherwise the EINVAL would be raised by libsystemd. --- src/sdbus/interface_generator.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/sdbus/interface_generator.py b/src/sdbus/interface_generator.py index 0c5930a..faea95c 100644 --- a/src/sdbus/interface_generator.py +++ b/src/sdbus/interface_generator.py @@ -277,11 +277,14 @@ def __init__(self, element: Element): self.iter_sub_elements(element) + def _can_use_unpivileged(self) -> bool: + return True + def _flags_iter(self) -> Iterator[str]: if self.is_deprecated: yield 'DbusDeprecatedFlag' - if not self.is_priveledged: + if not self.is_priveledged and self._can_use_unpivileged(): yield 'DbusUnprivilegedFlag' @property @@ -456,6 +459,9 @@ def __init__(self, element: Element): super().__init__(element) + def _can_use_unpivileged(self) -> bool: + return not self.is_read_only + def _flags_iter(self) -> Iterator[str]: emits_changed_str = self._EMITS_CHANGED_MAP.get(self.emits_changed) if emits_changed_str is not None: @@ -498,6 +504,9 @@ def __init__(self, element: Element): self.args: List[DbusArgsIntrospection] = [] super().__init__(element) + def _can_use_unpivileged(self) -> bool: + return False + def _parse_arg(self, arg: Element) -> None: new_arg = DbusArgsIntrospection(arg) From 98082ac4fa8584cd55ec00c188d4a394a73fd97f Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sat, 26 Oct 2024 21:23:10 +0100 Subject: [PATCH 126/188] Fix code generator not defaulting to emit changed for properties Properties emit changed signals by default but for systemd this is inverted as a flag is needed. --- src/sdbus/interface_generator.py | 16 ++++++++-------- test/test_interface_generator.py | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/sdbus/interface_generator.py b/src/sdbus/interface_generator.py index faea95c..b3a6ce9 100644 --- a/src/sdbus/interface_generator.py +++ b/src/sdbus/interface_generator.py @@ -432,12 +432,13 @@ def __repr__(self) -> str: class DbusPropertyIntrospection(DbusMemberAbstract): - _EMITS_CHANGED_MAP: \ - Dict[Union[bool, None, Literal['const', 'invalidates']], str] = { - True: 'DbusPropertyEmitsChangeFlag', - 'invalidates': 'DbusPropertyEmitsInvalidationFlag', - 'const': 'DbusPropertyConstFlag', - } + _EMITS_CHANGED_MAP: Dict[ + Union[bool, Literal['const', 'invalidates']], str + ] = { + True: 'DbusPropertyEmitsChangeFlag', + 'invalidates': 'DbusPropertyEmitsInvalidationFlag', + 'const': 'DbusPropertyConstFlag', + } def __init__(self, element: Element): if element.tag != 'property': @@ -445,8 +446,7 @@ def __init__(self, element: Element): self.dbus_signature = element.attrib['type'] - self.emits_changed: \ - Union[bool, Literal['const', 'invalidates'], None] = None + self.emits_changed: Union[bool, Literal['const', 'invalidates']] = True self.is_explicit = False access_type = element.attrib['access'] diff --git a/test/test_interface_generator.py b/test/test_interface_generator.py index aca9792..7156810 100644 --- a/test/test_interface_generator.py +++ b/test/test_interface_generator.py @@ -183,7 +183,7 @@ def test_parsing(self) -> None: elif test_property.method_name == 'Bar': self.assertEqual( test_property.emits_changed, - None, + True, ) elif test_property.method_name == 'FooInvalidates': self.assertEqual( From 0b15fa86ae5457c656cf235de75811c117be297d Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sat, 26 Oct 2024 21:39:58 +0100 Subject: [PATCH 127/188] Disable unprivileged flag for code generated properties Unprivileged flag can only be used with writeable properties but code generator does not generate setters. --- src/sdbus/interface_generator.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/sdbus/interface_generator.py b/src/sdbus/interface_generator.py index b3a6ce9..e7c3f9c 100644 --- a/src/sdbus/interface_generator.py +++ b/src/sdbus/interface_generator.py @@ -460,7 +460,10 @@ def __init__(self, element: Element): super().__init__(element) def _can_use_unpivileged(self) -> bool: - return not self.is_read_only + # Only properties that have setters defined can use the + # unprivileged flags. The code generator does NOT generate + # setters. + return False def _flags_iter(self) -> Iterator[str]: emits_changed_str = self._EMITS_CHANGED_MAP.get(self.emits_changed) From f77ff04471c6056184afcfd145b6049164d79b16 Mon Sep 17 00:00:00 2001 From: igo95862 Date: Wed, 30 Oct 2024 20:37:40 +0000 Subject: [PATCH 128/188] Add interface and member name override options to code generator `--select-*` options will select an interface and member and then the `--set-name` option can be used to set a particular name. --- docs/code_generator.rst | 58 +++++++++ src/sdbus/__main__.py | 255 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 311 insertions(+), 2 deletions(-) diff --git a/docs/code_generator.rst b/docs/code_generator.rst index d1978f7..dfe31d5 100644 --- a/docs/code_generator.rst +++ b/docs/code_generator.rst @@ -51,3 +51,61 @@ Multiple object paths can be passed which generates a file containing all interfaces encountered in the objects. Pass ``--system`` option to use system bus instead of session bus. + +Renaming interfaces and members +------------------------------- + +*New in version 0.13.0.* + +Some interface and member names might conflict with Python keywords when +converted from D-Bus introspection to Python code by gerator. The CLI interface +allow to override the particular interface and member names using the ``--select-*`` +and ``--set-name`` options. The selector options move the cursor to a particular +interface and member + +Available override options: + +* ``--set-name`` + Sets the name of currently selected element as it would + be in generated Python code. Can be used if either interface or + member is selected. + +* ``--select-interface`` + Selects the interface using its D-Bus name. + +* ``--select-method`` + Selects the method using its D-Bus name. + An interface must be selected first. + +* ``--select-property`` + Selects the property using its D-Bus name. + An interface must be selected first. + +* ``--select-signal`` + Selects the signal using its D-Bus name. + An interface must be selected first. + +For example, an ``org.example.Interface`` interface has a property called ``Class``. +When automatically converted the name will become ``class`` which is a reserved Python keyword. + +Using these CLI options it is possible to override the name of the property and class: + +.. code-block:: shell + + python -m sdbus gen-from-file \ + org.example.interface.xml \ + --select-interface org.example.Interface \ + --set-name Example \ + --select-property Class \ + --set-name example_class + +This will generate following Python code: + +.. code-block:: python + + class Example: + @dbus_property_async( + property_signature="s", + ) + def example_class(self) -> str: + raise NotImplementedError diff --git a/src/sdbus/__main__.py b/src/sdbus/__main__.py index b81cdbd..495cdc6 100644 --- a/src/sdbus/__main__.py +++ b/src/sdbus/__main__.py @@ -19,7 +19,8 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA from __future__ import annotations -from argparse import ArgumentParser +from argparse import SUPPRESS, Action, ArgumentParser +from dataclasses import dataclass, field from pathlib import Path from sys import stdout from typing import TYPE_CHECKING @@ -31,11 +32,84 @@ ) if TYPE_CHECKING: - from typing import List, Optional + from typing import Dict, List, Optional from .interface_generator import DbusInterfaceIntrospection +@dataclass +class RenameMember: + new_name: Optional[str] = None + current_arg: Optional[str] = None + arg_renames: Dict[str, str] = field(default_factory=dict) + + +@dataclass +class RenameInterface: + new_name: Optional[str] = None + current_member: Optional[RenameMember] = None + methods: Dict[str, RenameMember] = field(default_factory=dict) + properties: Dict[str, RenameMember] = field(default_factory=dict) + signals: Dict[str, RenameMember] = field(default_factory=dict) + + +@dataclass +class RenameRoot: + current_interface: Optional[RenameInterface] = None + interfaces: Dict[str, RenameInterface] = field(default_factory=dict) + + +rename_root = RenameRoot() + + +# def rename_args(member_rename): +# ... + + +def rename_members( + interface: DbusInterfaceIntrospection, + interface_rename: RenameInterface, +) -> None: + for m_member in interface.methods: + m_rename = interface_rename.methods.get(m_member.method_name) + if m_rename is None: + continue + + if m_rename.new_name is not None: + m_member.python_name = m_rename.new_name + + for p_member in interface.properties: + p_rename = interface_rename.properties.get(p_member.method_name) + if p_rename is None: + continue + + if p_rename.new_name is not None: + p_member.python_name = p_rename.new_name + + for s_member in interface.signals: + s_rename = interface_rename.signals.get(s_member.method_name) + if s_rename is None: + continue + + if s_rename.new_name is not None: + s_member.python_name = s_rename.new_name + + +def rename_interfaces( + interfaces: List[DbusInterfaceIntrospection] +) -> None: + for interface in interfaces: + dbus_interface_name = interface.interface_name + this_interface_rename = rename_root.interfaces.get(dbus_interface_name) + if this_interface_rename is None: + continue + + if this_interface_rename.new_name is not None: + interface.python_name = this_interface_rename.new_name + + rename_members(interface, this_interface_rename) + + def run_gen_from_connection( connection_name: str, object_paths: List[str], @@ -59,6 +133,8 @@ def run_gen_from_connection( itrospection = connection.dbus_introspect() interfaces.extend(interfaces_from_str(itrospection)) + rename_interfaces(interfaces) + stdout.write( generate_py_file( interfaces, @@ -78,6 +154,8 @@ def run_gen_from_file( for file in filenames: interfaces.extend(interfaces_from_file(file)) + rename_interfaces(interfaces) + stdout.write( generate_py_file( interfaces, @@ -87,6 +165,149 @@ def run_gen_from_file( ) +class ActionSelectInterface(Action): + def __call__( + self, + parser: ArgumentParser, + namespace: object, + values: object, + option_string: Optional[str] = None, + ) -> None: + if not isinstance(values, str): + raise TypeError( + f"Expected --select-interface to be string, got {values!r}" + ) + + interface_rename = rename_root.interfaces.get(values) + + if interface_rename is None: + interface_rename = RenameInterface() + rename_root.interfaces[values] = interface_rename + + rename_root.current_interface = interface_rename + + +class ActionSelectMethod(Action): + def __call__( + self, + parser: ArgumentParser, + namespace: object, + values: object, + option_string: Optional[str] = None, + ) -> None: + if not isinstance(values, str): + raise TypeError( + f"Expected --select-method to be string, got {values!r}" + ) + + current_interface = rename_root.current_interface + if current_interface is None: + raise ValueError( + "No D-Bus interface selected. " + "Use --select-interface option." + ) + + method_rename = current_interface.methods.get(values) + + if method_rename is None: + method_rename = RenameMember() + current_interface.methods[values] = method_rename + + current_interface.current_member = method_rename + + +class ActionSelectProperty(Action): + def __call__( + self, + parser: ArgumentParser, + namespace: object, + values: object, + option_string: Optional[str] = None, + ) -> None: + if not isinstance(values, str): + raise TypeError( + f"Expected --select-property to be string, got {values!r}" + ) + + current_interface = rename_root.current_interface + if current_interface is None: + raise ValueError( + "No D-Bus interface selected. " + "Use --select-interface option." + ) + + property_rename = current_interface.properties.get(values) + + if property_rename is None: + property_rename = RenameMember() + current_interface.properties[values] = property_rename + + current_interface.current_member = property_rename + + +class ActionSelectSignal(Action): + def __call__( + self, + parser: ArgumentParser, + namespace: object, + values: object, + option_string: Optional[str] = None, + ) -> None: + if not isinstance(values, str): + raise TypeError( + f"Expected --select-signal to be string, got {values!r}" + ) + + current_interface = rename_root.current_interface + if current_interface is None: + raise ValueError( + "No D-Bus interface selected. " + "Use --select-interface option." + ) + + signal_rename = current_interface.signals.get(values) + + if signal_rename is None: + signal_rename = RenameMember() + current_interface.signals[values] = signal_rename + + current_interface.current_member = signal_rename + + +class ActionSetName(Action): + def __call__( + self, + parser: ArgumentParser, + namespace: object, + values: object, + option_string: Optional[str] = None, + ) -> None: + if not isinstance(values, str): + raise TypeError( + f"Expected --set-name to be string, got {values!r}" + ) + + current_interface = rename_root.current_interface + current_member = ( + current_interface.current_member + if current_interface is not None + else None + ) + + if current_member is not None: + current_member.new_name = values + return + + if current_interface is not None: + current_interface.new_name = values + return + + raise ValueError( + "No D-Bus element to rename. " + "Use --select-* options to select element." + ) + + def generator_main(args: Optional[List[str]] = None) -> None: main_arg_parser = ArgumentParser( @@ -126,6 +347,36 @@ def generator_main(args: Optional[List[str]] = None) -> None: dest="do_async", help="Generate blocking interfaces", ) + subparser.add_argument( + "--select-interface", + action=ActionSelectInterface, + default=SUPPRESS, + help="Select D-Bus interface to adjust" + ) + subparser.add_argument( + "--select-method", + action=ActionSelectMethod, + default=SUPPRESS, + help="Select D-Bus method to adjust" + ) + subparser.add_argument( + "--select-property", + action=ActionSelectProperty, + default=SUPPRESS, + help="Select D-Bus property to adjust" + ) + subparser.add_argument( + "--select-signal", + action=ActionSelectSignal, + default=SUPPRESS, + help="Select D-Bus signal to adjust" + ) + subparser.add_argument( + "--set-name", + action=ActionSetName, + default=SUPPRESS, + help="Select D-Bus interface to adjust" + ) generate_from_file_parser.add_argument( 'filenames', type=Path, nargs='+', From 31894a8a48f817ad26e39a04c2df8587add9a521 Mon Sep 17 00:00:00 2001 From: igo95862 Date: Wed, 30 Oct 2024 20:44:22 +0000 Subject: [PATCH 129/188] Add missing 0.12.0 changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2fa73ac..b24cb18 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.12.0 + +No changes since 0.12.RC1. + ## 0.12.RC1 This version significantly reworked the internal undocumented classes From 82b379092c5ee5dd5748a8bd38eaf30b192fc8ac Mon Sep 17 00:00:00 2001 From: igo95862 Date: Thu, 31 Oct 2024 19:03:52 +0000 Subject: [PATCH 130/188] Add missing __all__ to sdbus.utils.inspect Only `inspect_dbus_path` is currently exported by it. --- src/sdbus/utils/inspect.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/sdbus/utils/inspect.py b/src/sdbus/utils/inspect.py index f34f550..a73e544 100644 --- a/src/sdbus/utils/inspect.py +++ b/src/sdbus/utils/inspect.py @@ -84,3 +84,8 @@ def inspect_dbus_path( return _inspect_dbus_path_local(obj, dbus_meta, bus) else: raise TypeError(f"Expected D-Bus object got {obj!r}") + + +__all__ = ( + "inspect_dbus_path", +) From bd88afa9f03f9ae0715856ce7932e996b77ee912 Mon Sep 17 00:00:00 2001 From: igo95862 Date: Thu, 31 Oct 2024 20:34:02 +0000 Subject: [PATCH 131/188] Version 0.13.0 --- CHANGELOG.md | 31 +++++++++++++++++++++++++++++++ setup.py | 2 +- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b24cb18..e494018 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,34 @@ +## 0.13.0 + +### Code generator improvements + +* Added interface and member renaming CLI options. `--select-interface`, `--select-method`, + `--select-property` and `--select-signal` will select a particular interface or member and + `--set-name` will set the selected interface or member Python name. +* Fix generated D-Bus properties not using emits changed flag by default. +* Fix generated D-Bus methods not using unprivileged flag by default. (reported by @damienklotz77) +* Generated methods and signals will now have result argument names set which will be shown + in the introspection. (requested by @colazzo) + +### New `sdbus.utils.inspect` submodule + +Contains inspection utilities. + +Current only provides the `inspect_dbus_path` function which will return +the D-Bus path of either proxy or exported object. (requested by ) + +### New `sdbus.utils.parse` submodule + +The existing `parse_properties_changed`, `parse_interfaces_added`, `parse_interfaces_removed` and +`parse_get_managed_objects` have been moved from from `sdbus.utils` to `sdbus.utils.parse`. + +For backwards compatibility `sdbus.utils` re-exports those functions but no new exports will be +added to it. + +### Fixes + +* Fix bus timeouts not being processed on time. (requested by @ofacklam) + ## 0.12.0 No changes since 0.12.RC1. diff --git a/setup.py b/setup.py index 0830ac1..f3928c7 100644 --- a/setup.py +++ b/setup.py @@ -96,7 +96,7 @@ def get_link_arguments() -> List[str]: 'Based on sd-bus from libsystemd.'), long_description=long_description, long_description_content_type='text/markdown', - version='0.12.0', + version='0.13.0', url='https://github.com/igo95862/python-sdbus', author='igo95862', author_email='igo95862@yandex.ru', From 79695f3d7412163009ffa50a59f1818542d0c44e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alan=20Dragomireck=C3=BD?= Date: Fri, 6 Dec 2024 13:11:16 +0100 Subject: [PATCH 132/188] Rename DbusSomething* to DbusMember* --- src/sdbus/dbus_common_elements.py | 14 ++++----- src/sdbus/dbus_proxy_async_interface_base.py | 30 ++++++++++---------- src/sdbus/dbus_proxy_async_method.py | 4 +-- src/sdbus/dbus_proxy_async_property.py | 4 +-- src/sdbus/dbus_proxy_async_signal.py | 4 +-- src/sdbus/dbus_proxy_sync_interface_base.py | 8 +++--- src/sdbus/dbus_proxy_sync_method.py | 4 +-- src/sdbus/dbus_proxy_sync_property.py | 4 +-- 8 files changed, 36 insertions(+), 36 deletions(-) diff --git a/src/sdbus/dbus_common_elements.py b/src/sdbus/dbus_common_elements.py index 7c8aa20..4c6fd69 100644 --- a/src/sdbus/dbus_common_elements.py +++ b/src/sdbus/dbus_common_elements.py @@ -49,16 +49,16 @@ T = TypeVar('T') -class DbusSomethingCommon: +class DbusMemberCommon: interface_name: str serving_enabled: bool -class DbusSomethingAsync(DbusSomethingCommon): +class DbusMemberAsync(DbusMemberCommon): ... -class DbusSomethingSync(DbusSomethingCommon): +class DbusMemberSync(DbusMemberCommon): ... @@ -83,7 +83,7 @@ def __new__(cls: Type[SelfMeta], name: str, ... for attr_name, attr in namespace.items(): - if not isinstance(attr, DbusSomethingCommon): + if not isinstance(attr, DbusMemberCommon): continue # TODO: Fix async metaclass copying all methods @@ -112,7 +112,7 @@ def __new__(cls: Type[SelfMeta], name: str, ) -class DbusMethodCommon(DbusSomethingCommon): +class DbusMethodCommon(DbusMemberCommon): def __init__( self, @@ -231,7 +231,7 @@ def _rebuild_args( return new_args_list -class DbusPropertyCommon(DbusSomethingCommon): +class DbusPropertyCommon(DbusMemberCommon): def __init__(self, property_name: Optional[str], property_signature: str, @@ -262,7 +262,7 @@ def __init__(self, self.flags = flags -class DbusSignalCommon(DbusSomethingCommon): +class DbusSignalCommon(DbusMemberCommon): def __init__(self, signal_name: Optional[str], signal_signature: str, diff --git a/src/sdbus/dbus_proxy_async_interface_base.py b/src/sdbus/dbus_proxy_async_interface_base.py index f13d162..bc60e3d 100644 --- a/src/sdbus/dbus_proxy_async_interface_base.py +++ b/src/sdbus/dbus_proxy_async_interface_base.py @@ -31,12 +31,12 @@ DbusClassMeta, DbusInterfaceMetaCommon, DbusLocalObjectMeta, + DbusMemberAsync, + DbusMemberCommon, + DbusMemberSync, DbusMethodOverride, DbusPropertyOverride, DbusRemoteObjectMeta, - DbusSomethingAsync, - DbusSomethingCommon, - DbusSomethingSync, ) from .dbus_common_funcs import get_default_bus from .dbus_proxy_async_method import DbusMethodAsync, DbusMethodAsyncLocalBind @@ -81,7 +81,7 @@ class DbusInterfaceMetaAsync(DbusInterfaceMetaCommon): def _process_dbus_method_override( override_attr_name: str, override: DbusMethodOverride[T], - mro_dbus_elements: Dict[str, DbusSomethingAsync], + mro_dbus_elements: Dict[str, DbusMemberAsync], ) -> DbusMethodAsync: try: original_method = mro_dbus_elements[override_attr_name] @@ -105,7 +105,7 @@ def _process_dbus_method_override( def _process_dbus_property_override( override_attr_name: str, override: DbusPropertyOverride[T], - mro_dbus_elements: Dict[str, DbusSomethingAsync], + mro_dbus_elements: Dict[str, DbusMemberAsync], ) -> DbusPropertyAsync[Any]: try: original_property = mro_dbus_elements[override_attr_name] @@ -137,11 +137,11 @@ def _check_collisions( cls, new_class_name: str, namespace: Dict[str, Any], - mro_dbus_elements: Dict[str, DbusSomethingAsync], + mro_dbus_elements: Dict[str, DbusMemberAsync], ) -> None: possible_collisions = namespace.keys() & mro_dbus_elements.keys() - new_overrides: Dict[str, DbusSomethingAsync] = {} + new_overrides: Dict[str, DbusMemberAsync] = {} for attr_name, attr in namespace.items(): if isinstance(attr, DbusMethodOverride): @@ -173,12 +173,12 @@ def _check_collisions( def _extract_dbus_elements( dbus_class: type, dbus_meta: DbusClassMeta, - ) -> Dict[str, DbusSomethingAsync]: - dbus_elements_map: Dict[str, DbusSomethingAsync] = {} + ) -> Dict[str, DbusMemberAsync]: + dbus_elements_map: Dict[str, DbusMemberAsync] = {} for attr_name in dbus_meta.python_attr_to_dbus_member.keys(): dbus_element = dbus_class.__dict__.get(attr_name) - if not isinstance(dbus_element, DbusSomethingAsync): + if not isinstance(dbus_element, DbusMemberAsync): raise TypeError( f"Expected async D-Bus element, got {dbus_element!r} " f"in class {dbus_class!r}" @@ -193,8 +193,8 @@ def _map_mro_dbus_elements( cls, new_class_name: str, base_classes: Iterable[type], - ) -> Dict[str, DbusSomethingAsync]: - all_python_dbus_map: Dict[str, DbusSomethingAsync] = {} + ) -> Dict[str, DbusMemberAsync]: + all_python_dbus_map: Dict[str, DbusMemberAsync] = {} possible_collisions: Set[str] = set() for c in base_classes: @@ -227,10 +227,10 @@ def _map_dbus_elements( meta: DbusClassMeta, interface_name: str, ) -> None: - if not isinstance(attr, DbusSomethingCommon): + if not isinstance(attr, DbusMemberCommon): return - if isinstance(attr, DbusSomethingSync): + if isinstance(attr, DbusMemberSync): raise TypeError( "Can't mix blocking methods in " f"async interface: {attr_name!r}" @@ -347,7 +347,7 @@ def export_to_dbus( interface_map: Dict[str, List[DbusBindedAsync]] = {} for key, value in getmembers(self): - assert not isinstance(value, DbusSomethingAsync) + assert not isinstance(value, DbusMemberAsync) if isinstance(value, DbusMethodAsyncLocalBind): interface_name = value.dbus_method.interface_name diff --git a/src/sdbus/dbus_proxy_async_method.py b/src/sdbus/dbus_proxy_async_method.py index d4c6138..46c7661 100644 --- a/src/sdbus/dbus_proxy_async_method.py +++ b/src/sdbus/dbus_proxy_async_method.py @@ -27,10 +27,10 @@ from .dbus_common_elements import ( DbusBindedAsync, + DbusMemberAsync, DbusMethodCommon, DbusMethodOverride, DbusRemoteObjectMeta, - DbusSomethingAsync, ) from .dbus_exceptions import DbusFailedError from .sd_bus_internals import DbusNoReplyFlag @@ -52,7 +52,7 @@ def get_current_message() -> SdBusMessage: return CURRENT_MESSAGE.get() -class DbusMethodAsync(DbusMethodCommon, DbusSomethingAsync): +class DbusMethodAsync(DbusMethodCommon, DbusMemberAsync): @overload def __get__( diff --git a/src/sdbus/dbus_proxy_async_property.py b/src/sdbus/dbus_proxy_async_property.py index b6ebdee..b293389 100644 --- a/src/sdbus/dbus_proxy_async_property.py +++ b/src/sdbus/dbus_proxy_async_property.py @@ -26,10 +26,10 @@ from .dbus_common_elements import ( DbusBindedAsync, + DbusMemberAsync, DbusPropertyCommon, DbusPropertyOverride, DbusRemoteObjectMeta, - DbusSomethingAsync, ) if TYPE_CHECKING: @@ -42,7 +42,7 @@ T = TypeVar('T') -class DbusPropertyAsync(DbusSomethingAsync, DbusPropertyCommon, Generic[T]): +class DbusPropertyAsync(DbusMemberAsync, DbusPropertyCommon, Generic[T]): def __init__( self, property_name: Optional[str], diff --git a/src/sdbus/dbus_proxy_async_signal.py b/src/sdbus/dbus_proxy_async_signal.py index 0aca3e7..43ff4fc 100644 --- a/src/sdbus/dbus_proxy_async_signal.py +++ b/src/sdbus/dbus_proxy_async_signal.py @@ -36,9 +36,9 @@ from .dbus_common_elements import ( DbusBindedAsync, DbusLocalObjectMeta, + DbusMemberAsync, DbusRemoteObjectMeta, DbusSignalCommon, - DbusSomethingAsync, ) from .dbus_common_funcs import get_default_bus @@ -52,7 +52,7 @@ T = TypeVar('T') -class DbusSignalAsync(DbusSomethingAsync, DbusSignalCommon, Generic[T]): +class DbusSignalAsync(DbusMemberAsync, DbusSignalCommon, Generic[T]): def __init__( self, diff --git a/src/sdbus/dbus_proxy_sync_interface_base.py b/src/sdbus/dbus_proxy_sync_interface_base.py index e749fb8..783256e 100644 --- a/src/sdbus/dbus_proxy_sync_interface_base.py +++ b/src/sdbus/dbus_proxy_sync_interface_base.py @@ -26,9 +26,9 @@ from .dbus_common_elements import ( DbusClassMeta, DbusInterfaceMetaCommon, + DbusMemberAsync, + DbusMemberCommon, DbusRemoteObjectMeta, - DbusSomethingAsync, - DbusSomethingCommon, ) from .dbus_proxy_sync_method import DbusMethodSync from .dbus_proxy_sync_property import DbusPropertySync @@ -109,10 +109,10 @@ def _map_dbus_elements( attr: Any, meta: DbusClassMeta, ) -> None: - if not isinstance(attr, DbusSomethingCommon): + if not isinstance(attr, DbusMemberCommon): return - if isinstance(attr, DbusSomethingAsync): + if isinstance(attr, DbusMemberAsync): raise TypeError( f"Can't mix async methods in sync interface: {attr_name!r}" ) diff --git a/src/sdbus/dbus_proxy_sync_method.py b/src/sdbus/dbus_proxy_sync_method.py index 4d4c487..4bf140e 100644 --- a/src/sdbus/dbus_proxy_sync_method.py +++ b/src/sdbus/dbus_proxy_sync_method.py @@ -25,8 +25,8 @@ from .dbus_common_elements import ( DbusBindedSync, + DbusMemberSync, DbusMethodCommon, - DbusSomethingSync, ) if TYPE_CHECKING: @@ -37,7 +37,7 @@ T = TypeVar('T') -class DbusMethodSync(DbusMethodCommon, DbusSomethingSync): +class DbusMethodSync(DbusMethodCommon, DbusMemberSync): def __get__(self, obj: DbusInterfaceBase, obj_class: Optional[Type[DbusInterfaceBase]] = None, diff --git a/src/sdbus/dbus_proxy_sync_property.py b/src/sdbus/dbus_proxy_sync_property.py index 6f4f303..4cbb5af 100644 --- a/src/sdbus/dbus_proxy_sync_property.py +++ b/src/sdbus/dbus_proxy_sync_property.py @@ -23,7 +23,7 @@ from types import FunctionType from typing import TYPE_CHECKING, Generic, TypeVar, cast -from .dbus_common_elements import DbusPropertyCommon, DbusSomethingSync +from .dbus_common_elements import DbusMemberSync, DbusPropertyCommon from .dbus_common_funcs import _check_sync_in_async_env if TYPE_CHECKING: @@ -35,7 +35,7 @@ T = TypeVar('T') -class DbusPropertySync(DbusPropertyCommon, DbusSomethingSync, Generic[T]): +class DbusPropertySync(DbusPropertyCommon, DbusMemberSync, Generic[T]): def __init__( self, property_name: Optional[str], From 1fbc7ce9f9f6e406dc7747783af6f3fa750627ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alan=20Dragomireck=C3=BD?= Date: Fri, 13 Dec 2024 13:13:45 +0100 Subject: [PATCH 133/188] Rename *Binded types to *Bound and their subclasses --- src/sdbus/dbus_common_elements.py | 4 ++-- src/sdbus/dbus_proxy_async_interface_base.py | 22 ++++++++++---------- src/sdbus/dbus_proxy_async_method.py | 12 +++++------ src/sdbus/dbus_proxy_async_property.py | 16 +++++++------- src/sdbus/dbus_proxy_async_signal.py | 16 +++++++------- src/sdbus/dbus_proxy_sync_method.py | 6 +++--- src/sdbus/unittest.py | 17 +++++++-------- 7 files changed, 45 insertions(+), 48 deletions(-) diff --git a/src/sdbus/dbus_common_elements.py b/src/sdbus/dbus_common_elements.py index 4c6fd69..725674e 100644 --- a/src/sdbus/dbus_common_elements.py +++ b/src/sdbus/dbus_common_elements.py @@ -291,11 +291,11 @@ def __init__(self, self.__annotations__ = original_method.__annotations__ -class DbusBindedAsync: +class DbusBoundAsync: ... -class DbusBindedSync: +class DbusBoundSync: ... diff --git a/src/sdbus/dbus_proxy_async_interface_base.py b/src/sdbus/dbus_proxy_async_interface_base.py index bc60e3d..9f48121 100644 --- a/src/sdbus/dbus_proxy_async_interface_base.py +++ b/src/sdbus/dbus_proxy_async_interface_base.py @@ -39,12 +39,12 @@ DbusRemoteObjectMeta, ) from .dbus_common_funcs import get_default_bus -from .dbus_proxy_async_method import DbusMethodAsync, DbusMethodAsyncLocalBind +from .dbus_proxy_async_method import DbusLocalMethodAsync, DbusMethodAsync from .dbus_proxy_async_property import ( + DbusLocalPropertyAsync, DbusPropertyAsync, - DbusPropertyAsyncLocalBind, ) -from .dbus_proxy_async_signal import DbusSignalAsync, DbusSignalAsyncLocalBind +from .dbus_proxy_async_signal import DbusLocalSignalAsync, DbusSignalAsync from .sd_bus_internals import SdBusInterface if TYPE_CHECKING: @@ -61,7 +61,7 @@ Union, ) - from .dbus_common_elements import DbusBindedAsync + from .dbus_common_elements import DbusBoundAsync from .sd_bus_internals import SdBus, SdBusSlot T = TypeVar('T') @@ -344,20 +344,20 @@ def export_to_dbus( local_object_meta.attached_bus = bus local_object_meta.serving_object_path = object_path # TODO: can be optimized with a single loop - interface_map: Dict[str, List[DbusBindedAsync]] = {} + interface_map: Dict[str, List[DbusBoundAsync]] = {} for key, value in getmembers(self): assert not isinstance(value, DbusMemberAsync) - if isinstance(value, DbusMethodAsyncLocalBind): + if isinstance(value, DbusLocalMethodAsync): interface_name = value.dbus_method.interface_name if not value.dbus_method.serving_enabled: continue - elif isinstance(value, DbusPropertyAsyncLocalBind): + elif isinstance(value, DbusLocalPropertyAsync): interface_name = value.dbus_property.interface_name if not value.dbus_property.serving_enabled: continue - elif isinstance(value, DbusSignalAsyncLocalBind): + elif isinstance(value, DbusLocalSignalAsync): interface_name = value.dbus_signal.interface_name if not value.dbus_signal.serving_enabled: continue @@ -375,7 +375,7 @@ def export_to_dbus( for interface_name, member_list in interface_map.items(): new_interface = SdBusInterface() for dbus_something in member_list: - if isinstance(dbus_something, DbusMethodAsyncLocalBind): + if isinstance(dbus_something, DbusLocalMethodAsync): new_interface.add_method( dbus_something.dbus_method.method_name, dbus_something.dbus_method.input_signature, @@ -385,7 +385,7 @@ def export_to_dbus( dbus_something.dbus_method.flags, dbus_something._dbus_reply_call, ) - elif isinstance(dbus_something, DbusPropertyAsyncLocalBind): + elif isinstance(dbus_something, DbusLocalPropertyAsync): getter = dbus_something._dbus_reply_get dbus_property = dbus_something.dbus_property @@ -405,7 +405,7 @@ def export_to_dbus( setter, dbus_property.flags, ) - elif isinstance(dbus_something, DbusSignalAsyncLocalBind): + elif isinstance(dbus_something, DbusLocalSignalAsync): new_interface.add_signal( dbus_something.dbus_signal.signal_name, dbus_something.dbus_signal.signal_signature, diff --git a/src/sdbus/dbus_proxy_async_method.py b/src/sdbus/dbus_proxy_async_method.py index 46c7661..d671df6 100644 --- a/src/sdbus/dbus_proxy_async_method.py +++ b/src/sdbus/dbus_proxy_async_method.py @@ -26,7 +26,7 @@ from weakref import ref as weak_ref from .dbus_common_elements import ( - DbusBindedAsync, + DbusBoundAsync, DbusMemberAsync, DbusMethodCommon, DbusMethodOverride, @@ -78,20 +78,20 @@ def __get__( if obj is not None: dbus_meta = obj._dbus if isinstance(dbus_meta, DbusRemoteObjectMeta): - return DbusMethodAsyncProxyBind(self, dbus_meta) + return DbusProxyMethodAsync(self, dbus_meta) else: - return DbusMethodAsyncLocalBind(self, obj) + return DbusLocalMethodAsync(self, obj) else: return self -class DbusMethodAsyncBaseBind(DbusBindedAsync): +class DbusBoundMethodAsyncBase(DbusBoundAsync): def __call__(self, *args: Any, **kwargs: Any) -> Any: raise NotImplementedError -class DbusMethodAsyncProxyBind(DbusMethodAsyncBaseBind): +class DbusProxyMethodAsync(DbusBoundMethodAsyncBase): def __init__( self, dbus_method: DbusMethodAsync, @@ -145,7 +145,7 @@ def __call__(self, *args: Any, **kwargs: Any) -> Any: return self._dbus_async_call(new_call_message) -class DbusMethodAsyncLocalBind(DbusMethodAsyncBaseBind): +class DbusLocalMethodAsync(DbusBoundMethodAsyncBase): def __init__( self, dbus_method: DbusMethodAsync, diff --git a/src/sdbus/dbus_proxy_async_property.py b/src/sdbus/dbus_proxy_async_property.py index b293389..9cac940 100644 --- a/src/sdbus/dbus_proxy_async_property.py +++ b/src/sdbus/dbus_proxy_async_property.py @@ -25,7 +25,7 @@ from weakref import ref as weak_ref from .dbus_common_elements import ( - DbusBindedAsync, + DbusBoundAsync, DbusMemberAsync, DbusPropertyCommon, DbusPropertyOverride, @@ -84,20 +84,20 @@ def __get__( self, obj: DbusInterfaceBaseAsync, obj_class: Type[DbusInterfaceBaseAsync], - ) -> DbusPropertyAsyncBaseBind[T]: + ) -> DbusBoundPropertyAsyncBase[T]: ... def __get__( self, obj: Optional[DbusInterfaceBaseAsync], obj_class: Optional[Type[DbusInterfaceBaseAsync]] = None, - ) -> Union[DbusPropertyAsyncBaseBind[T], DbusPropertyAsync[T]]: + ) -> Union[DbusBoundPropertyAsyncBase[T], DbusPropertyAsync[T]]: if obj is not None: dbus_meta = obj._dbus if isinstance(dbus_meta, DbusRemoteObjectMeta): - return DbusPropertyAsyncProxyBind(self, dbus_meta) + return DbusProxyPropertyAsync(self, dbus_meta) else: - return DbusPropertyAsyncLocalBind(self, obj) + return DbusLocalPropertyAsync(self, obj) else: return self @@ -126,7 +126,7 @@ def setter_private( self.property_setter_is_public = False -class DbusPropertyAsyncBaseBind(DbusBindedAsync, Awaitable[T]): +class DbusBoundPropertyAsyncBase(DbusBoundAsync, Awaitable[T]): def __await__(self) -> Generator[Any, None, T]: return self.get_async().__await__() @@ -137,7 +137,7 @@ async def set_async(self, complete_object: T) -> None: raise NotImplementedError -class DbusPropertyAsyncProxyBind(DbusPropertyAsyncBaseBind[T]): +class DbusProxyPropertyAsync(DbusBoundPropertyAsyncBase[T]): def __init__( self, dbus_property: DbusPropertyAsync[T], @@ -179,7 +179,7 @@ async def set_async(self, complete_object: T) -> None: await bus.call_async(new_set_message) -class DbusPropertyAsyncLocalBind(DbusPropertyAsyncBaseBind[T]): +class DbusLocalPropertyAsync(DbusBoundPropertyAsyncBase[T]): def __init__( self, dbus_property: DbusPropertyAsync[T], diff --git a/src/sdbus/dbus_proxy_async_signal.py b/src/sdbus/dbus_proxy_async_signal.py index 43ff4fc..8de4c25 100644 --- a/src/sdbus/dbus_proxy_async_signal.py +++ b/src/sdbus/dbus_proxy_async_signal.py @@ -34,7 +34,7 @@ from weakref import WeakSet from .dbus_common_elements import ( - DbusBindedAsync, + DbusBoundAsync, DbusLocalObjectMeta, DbusMemberAsync, DbusRemoteObjectMeta, @@ -85,20 +85,20 @@ def __get__( self, obj: DbusInterfaceBaseAsync, obj_class: Type[DbusInterfaceBaseAsync], - ) -> DbusSignalAsyncBaseBind[T]: + ) -> DbusBoundSignalAsyncBase[T]: ... def __get__( self, obj: Optional[DbusInterfaceBaseAsync], obj_class: Optional[Type[DbusInterfaceBaseAsync]] = None, - ) -> Union[DbusSignalAsyncBaseBind[T], DbusSignalAsync[T]]: + ) -> Union[DbusBoundSignalAsyncBase[T], DbusSignalAsync[T]]: if obj is not None: dbus_meta = obj._dbus if isinstance(dbus_meta, DbusRemoteObjectMeta): - return DbusSignalAsyncProxyBind(self, dbus_meta) + return DbusProxySignalAsync(self, dbus_meta) else: - return DbusSignalAsyncLocalBind(self, dbus_meta) + return DbusLocalSignalAsync(self, dbus_meta) else: return self @@ -131,7 +131,7 @@ async def catch_anywhere( ) -class DbusSignalAsyncBaseBind(DbusBindedAsync, AsyncIterable[T], Generic[T]): +class DbusBoundSignalAsyncBase(DbusBoundAsync, AsyncIterable[T], Generic[T]): async def catch(self) -> AsyncIterator[T]: raise NotImplementedError yield cast(T, None) @@ -150,7 +150,7 @@ def emit(self, args: T) -> None: raise NotImplementedError -class DbusSignalAsyncProxyBind(DbusSignalAsyncBaseBind[T]): +class DbusProxySignalAsync(DbusBoundSignalAsyncBase[T]): def __init__( self, dbus_signal: DbusSignalAsync[T], @@ -224,7 +224,7 @@ def emit(self, args: T) -> None: raise RuntimeError("Cannot emit signal from D-Bus proxy.") -class DbusSignalAsyncLocalBind(DbusSignalAsyncBaseBind[T]): +class DbusLocalSignalAsync(DbusBoundSignalAsyncBase[T]): def __init__( self, dbus_signal: DbusSignalAsync[T], diff --git a/src/sdbus/dbus_proxy_sync_method.py b/src/sdbus/dbus_proxy_sync_method.py index 4bf140e..82e2d71 100644 --- a/src/sdbus/dbus_proxy_sync_method.py +++ b/src/sdbus/dbus_proxy_sync_method.py @@ -24,7 +24,7 @@ from typing import TYPE_CHECKING, TypeVar, cast from .dbus_common_elements import ( - DbusBindedSync, + DbusBoundSync, DbusMemberSync, DbusMethodCommon, ) @@ -42,10 +42,10 @@ def __get__(self, obj: DbusInterfaceBase, obj_class: Optional[Type[DbusInterfaceBase]] = None, ) -> Callable[..., Any]: - return DbusMethodSyncBinded(self, obj) + return DbusLocalMethodSync(self, obj) -class DbusMethodSyncBinded(DbusBindedSync): +class DbusLocalMethodSync(DbusBoundSync): def __init__(self, dbus_method: DbusMethodSync, interface: DbusInterfaceBase): diff --git a/src/sdbus/unittest.py b/src/sdbus/unittest.py index 90702e8..8b9a2ba 100644 --- a/src/sdbus/unittest.py +++ b/src/sdbus/unittest.py @@ -33,10 +33,7 @@ from weakref import ref as weak_ref from .dbus_common_funcs import set_default_bus -from .dbus_proxy_async_signal import ( - DbusSignalAsyncLocalBind, - DbusSignalAsyncProxyBind, -) +from .dbus_proxy_async_signal import DbusLocalSignalAsync, DbusProxySignalAsync from .sd_bus_internals import SdBusMessage, sd_bus_open_user if TYPE_CHECKING: @@ -51,8 +48,8 @@ ) from .dbus_proxy_async_signal import ( + DbusBoundSignalAsyncBase, DbusSignalAsync, - DbusSignalAsyncBaseBind, ) from .sd_bus_internals import SdBus, SdBusSlot @@ -124,7 +121,7 @@ def __init__( self, timeout: Union[int, float], bus: SdBus, - remote_signal: DbusSignalAsyncProxyBind[Any], + remote_signal: DbusProxySignalAsync[Any], ): super().__init__(timeout) self._bus = bus @@ -156,7 +153,7 @@ class DbusSignalRecorderLocal(DbusSignalRecorderBase): def __init__( self, timeout: Union[int, float], - local_signal: DbusSignalAsyncLocalBind[Any], + local_signal: DbusLocalSignalAsync[Any], ): super().__init__(timeout) self._local_signal_ref: weak_ref[DbusSignalAsync[Any]] = ( @@ -240,13 +237,13 @@ async def asyncSetUp(self) -> None: def assertDbusSignalEmits( self, - signal: DbusSignalAsyncBaseBind[Any], + signal: DbusBoundSignalAsyncBase[Any], timeout: Union[int, float] = 1, ) -> AsyncContextManager[DbusSignalRecorderBase]: - if isinstance(signal, DbusSignalAsyncLocalBind): + if isinstance(signal, DbusLocalSignalAsync): return DbusSignalRecorderLocal(timeout, signal) - elif isinstance(signal, DbusSignalAsyncProxyBind): + elif isinstance(signal, DbusProxySignalAsync): return DbusSignalRecorderRemote(timeout, self.bus, signal) else: raise TypeError("Unknown or unsupported signal class.") From 3b0b21277bf6b12d4aa6342fdf7556c33b149cd7 Mon Sep 17 00:00:00 2001 From: igo95862 Date: Wed, 18 Dec 2024 11:07:23 +0000 Subject: [PATCH 134/188] Increase github actions Ubuntu version to 22.04 The plan is increasing minimum Python version to 3.9 in the next release. 20.04 uses Python 3.8 and 22.04 uses 3.10. --- .github/workflows/ubuntu_pypi_test.yml | 2 +- .github/workflows/ubuntu_test.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ubuntu_pypi_test.yml b/.github/workflows/ubuntu_pypi_test.yml index 816008d..1698b15 100644 --- a/.github/workflows/ubuntu_pypi_test.yml +++ b/.github/workflows/ubuntu_pypi_test.yml @@ -9,7 +9,7 @@ on: jobs: run: name: Install from PyPI and run unit tests - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - name: Checkout uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 diff --git a/.github/workflows/ubuntu_test.yml b/.github/workflows/ubuntu_test.yml index 6c9b019..3cd0950 100644 --- a/.github/workflows/ubuntu_test.yml +++ b/.github/workflows/ubuntu_test.yml @@ -8,7 +8,7 @@ on: jobs: unlimited: name: Run build and unit tests. (unlimited API) - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - name: Checkout uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 @@ -25,7 +25,7 @@ jobs: PYTHONPATH=./build-lib python3 -m unittest --verbose limited: name: Run build and unit tests. (limited API) - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - name: Checkout uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 From 7a85f12cf0eaabbb68f4b0ac91b924902bf134e9 Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sat, 11 Jan 2025 18:17:28 +0000 Subject: [PATCH 135/188] Increase minimum Python version to 3.9 Python 3.8 support ended in October 2024. Python 3.9 will be supported until October 2025. https://devguide.python.org/versions/ --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index f3928c7..f8906ec 100644 --- a/setup.py +++ b/setup.py @@ -142,7 +142,7 @@ def get_link_arguments() -> List[str]: 'py.typed', ], }, - python_requires='>=3.7', + python_requires='>=3.9', ext_modules=[ Extension( 'sdbus.sd_bus_internals', From 5acc62f0a25104771c6932102ca0117af51da17f Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sun, 26 Jan 2025 20:17:02 +0000 Subject: [PATCH 136/188] Use standard collections type hinting generics Since Python 3.9 the standard collections like `list` or `dict` can be type hinted instead of `typing.List` or `typing.Dict`. Also use `collections.abc` instead of `typing` analogues. Interface generator still generates code using `typing` but it will be changed in the future. --- docs/asyncio_api.rst | 22 ++++---- docs/asyncio_quick.rst | 4 +- docs/autodoc.rst | 2 +- docs/sync_api.rst | 14 +++--- docs/sync_quick.rst | 2 +- docs/unittest.rst | 4 +- docs/utils.rst | 8 +-- setup.py | 10 ++-- src/sdbus/__main__.py | 24 ++++----- src/sdbus/autodoc.py | 4 +- src/sdbus/dbus_common_elements.py | 28 ++++------- src/sdbus/dbus_common_funcs.py | 9 ++-- src/sdbus/dbus_exceptions.py | 6 +-- src/sdbus/dbus_proxy_async_interface_base.py | 53 ++++++++------------ src/sdbus/dbus_proxy_async_interfaces.py | 14 +++--- src/sdbus/dbus_proxy_async_method.py | 9 ++-- src/sdbus/dbus_proxy_async_object_manager.py | 11 ++-- src/sdbus/dbus_proxy_async_property.py | 12 +++-- src/sdbus/dbus_proxy_async_signal.py | 28 ++++------- src/sdbus/dbus_proxy_sync_interface_base.py | 30 ++++------- src/sdbus/dbus_proxy_sync_interfaces.py | 10 ++-- src/sdbus/dbus_proxy_sync_method.py | 5 +- src/sdbus/dbus_proxy_sync_property.py | 5 +- src/sdbus/interface_generator.py | 50 ++++++++---------- src/sdbus/sd_bus_internals.py | 44 +++++++--------- src/sdbus/unittest.py | 18 +++---- src/sdbus/utils/parse.py | 50 ++++++++---------- src/sdbus_async/dbus_daemon/__init__.py | 12 ++--- src/sdbus_block/dbus_daemon/__init__.py | 10 ++-- test/leak_tests.py | 4 +- test/test_read_write_dbus_types.py | 5 +- test/test_sdbus_async.py | 22 ++++---- test/test_sdbus_async_introspection.py | 9 +--- test/test_sdbus_block.py | 1 - test/test_typing.py | 17 +++---- tools/run_py_linters.py | 7 ++- wheel-build/run_inside_container.py | 9 ++-- 37 files changed, 249 insertions(+), 323 deletions(-) diff --git a/docs/asyncio_api.rst b/docs/asyncio_api.rst index ccffeee..0b94711 100644 --- a/docs/asyncio_api.rst +++ b/docs/asyncio_api.rst @@ -68,10 +68,10 @@ Classes either raise an ``"error"`` (default), ``"ignore"`` the property or ``"reuse"`` the D-Bus name for the member. :return: dictionary of properties - :rtype: Dict[str, Any] + :rtype: dict[str, Any] .. py:attribute:: properties_changed - :type: Tuple[str, Dict[str, Tuple[str, Any]], List[str]] + :type: tuple[str, dict[str, tuple[str, Any]], list[str]] Signal when one of the objects properties changes. @@ -83,11 +83,11 @@ Classes Interface name : str Name of the interface where property changed - Changed properties : Dict[str, Tuple[str, Any]] + Changed properties : dict[str, tuple[str, Any]] Dictionary there keys are names of properties changed and values are variants of new value. - Invalidated properties : List[str] + Invalidated properties : list[str] List of property names changed but no new value had been provided .. py:method:: _proxify(bus, service_name, object_path) @@ -199,12 +199,12 @@ Classes Triple nested dictionary that contains all the objects paths with their properties values. - Dict[ObjectPath, Dict[InterfaceName, Dict[PropertyName, PropertyValue]]] + dict[ObjectPath, dict[InterfaceName, dict[PropertyName, PropertyValue]]] - :rtype: Dict[str, Dict[str, Dict[str, Any]]] + :rtype: dict[str, dict[str, dict[str, Any]]] .. py:attribute:: interfaces_added - :type: Tuple[str, Dict[str, Dict[str, Any]]] + :type: tuple[str, dict[str, dict[str, Any]]] Signal when a new object is added or and existing object gains a new interface. @@ -217,11 +217,11 @@ Classes Object path : str Path to object that was added or modified. - Object interfaces and properties : Dict[str, Dict[str, Any]]] - Dict[InterfaceName, Dict[PropertyName, PropertyValue]] + Object interfaces and properties : dict[str, dict[str, Any]]] + dict[InterfaceName, dict[PropertyName, PropertyValue]] .. py:attribute:: interfaces_removed - :type: Tuple[str, List[str]] + :type: tuple[str, list[str]] Signal when existing object or and interface of existing object is removed. @@ -234,7 +234,7 @@ Classes Object path : str Path to object that was removed or modified. - Interfaces list : List[str] + Interfaces list : list[str] Interfaces names that were removed. .. py:method:: export_with_manager(object_path, object_to_export, bus) diff --git a/docs/asyncio_quick.rst b/docs/asyncio_quick.rst index c035164..070f245 100644 --- a/docs/asyncio_quick.rst +++ b/docs/asyncio_quick.rst @@ -50,7 +50,7 @@ Example: :: # Signal with a list of strings @dbus_signal_async('as') - def str_signal(self) -> List[str]: + def str_signal(self) -> list[str]: raise NotImplementedError Initiating proxy @@ -342,7 +342,7 @@ Example: :: ): @dbus_method_async('as', 's') - async def join_str(self, str_array: List[str]) -> str: + async def join_str(self, str_array: list[str]) -> str: return ''.join(str_array) diff --git a/docs/autodoc.rst b/docs/autodoc.rst index 1fbc8e4..be35525 100644 --- a/docs/autodoc.rst +++ b/docs/autodoc.rst @@ -51,7 +51,7 @@ stub function. .. code-block:: python @dbus_property_async('as') - def features(self) -> List[str]: + def features(self) -> list[str]: """List of D-Bus daemon features. Features include: diff --git a/docs/sync_api.rst b/docs/sync_api.rst index 41a27c9..641949e 100644 --- a/docs/sync_api.rst +++ b/docs/sync_api.rst @@ -72,7 +72,7 @@ Classes either raise an ``"error"`` (default), ``"ignore"`` the property or ``"reuse"`` the D-Bus name for the member. :return: dictionary of properties - :rtype: Dict[str, Any] + :rtype: dict[str, Any] Example: :: @@ -91,12 +91,12 @@ Classes # Method that does not take any arguments and returns a list of str @dbus_method() - def get_capabilities(self) -> List[str]: + def get_capabilities(self) -> list[str]: raise NotImplementedError # Method that takes a dict of {str: str} and returns an int @dbus_method('a{ss}') - def count_entries(self, a_dict: Dict[str, str]) -> int: + def count_entries(self, a_dict: dict[str, str]) -> int: raise NotImplementedError # Read only property of int @@ -124,9 +124,9 @@ Classes Triple nested dictionary that contains all the objects paths with their properties values. - Dict[ObjectPath, Dict[InterfaceName, Dict[PropertyName, PropertyValue]]] + dict[ObjectPath, dict[InterfaceName, dict[PropertyName, PropertyValue]]] - :rtype: Dict[str, Dict[str, Dict[str, Any]]] + :rtype: dict[str, dict[str, dict[str, Any]]] Decorators +++++++++++++++ @@ -168,12 +168,12 @@ Decorators # Method that does not take any arguments and returns a list of str @dbus_method() - def get_capabilities(self) -> List[str]: + def get_capabilities(self) -> list[str]: raise NotImplementedError # Method that takes a dict of {str: str} and returns an int @dbus_method('a{ss}') - def count_entries(self, a_dict: Dict[str, str]) -> int: + def count_entries(self, a_dict: dict[str, str]) -> int: raise NotImplementedError Calling methods example:: diff --git a/docs/sync_quick.rst b/docs/sync_quick.rst index 8fd79c6..23f0ccc 100644 --- a/docs/sync_quick.rst +++ b/docs/sync_quick.rst @@ -157,7 +157,7 @@ Example: :: ): @dbus_method('as') - def test_method(self, str_array: List[str]) -> None: + def test_method(self, str_array: list[str]) -> None: raise NotImplementedError diff --git a/docs/unittest.rst b/docs/unittest.rst index 100b653..ab0a200 100644 --- a/docs/unittest.rst +++ b/docs/unittest.rst @@ -30,7 +30,7 @@ Python-sdbus provides several utilities to enable unit testing. """Uppercase the input""" return string.upper() - def initialize_object() -> Tuple[TestInterface, TestInterface]: + def initialize_object() -> tuple[TestInterface, TestInterface]: test_object = TestInterface() test_object.export_to_dbus('/') @@ -74,7 +74,7 @@ Python-sdbus provides several utilities to enable unit testing. The object returned by context manager has following attributes: .. py:attribute:: output - :type: List[Any] + :type: list[Any] List of captured data. diff --git a/docs/utils.rst b/docs/utils.rst index 9644c89..b1b3b4b 100644 --- a/docs/utils.rst +++ b/docs/utils.rst @@ -21,7 +21,7 @@ Available under ``sdbus.utils.parse`` subpackage. :param str on_unknown_member: If an unknown D-Bus property was encountered either raise an ``"error"`` (default), ``"ignore"`` the property or ``"reuse"`` the D-Bus name for the member. - :rtype: Dict[str, Any] + :rtype: dict[str, Any] :returns: Dictionary of changed properties with keys translated to python names. Invalidated properties will have value of None. @@ -42,7 +42,7 @@ Available under ``sdbus.utils.parse`` subpackage. :param str on_unknown_member: If an unknown D-Bus property was encountered either raise an ``"error"`` (default), ``"ignore"`` the property or ``"reuse"`` the D-Bus name for the member. - :rtype: Tuple[str, Optional[Type[DbusInterfaceBaseAsync]], Dict[str, Any]] + :rtype: tuple[str, Optional[type[DbusInterfaceBaseAsync]], dict[str, Any]] :returns: Path of new added object, object's class (or ``None``) and dictionary of python translated members and their values. @@ -60,7 +60,7 @@ Available under ``sdbus.utils.parse`` subpackage. :param str on_unknown_member: If an unknown D-Bus interface was encountered either raise an ``"error"`` (default) or return ``"none"`` instead of interface class. - :rtype: Tuple[str, Optional[Type[DbusInterfaceBaseAsync]]] + :rtype: tuple[str, Optional[type[DbusInterfaceBaseAsync]]] :returns: Path of removed object and object's class (or ``None``). .. py:function:: parse_get_managed_objects(interfaces, managed_objects_data, on_unknown_interface='error', on_unknown_member='error') @@ -80,7 +80,7 @@ Available under ``sdbus.utils.parse`` subpackage. :param str on_unknown_member: If an unknown D-Bus property was encountered either raise an ``"error"`` (default), ``"ignore"`` the property or ``"reuse"`` the D-Bus name for the member. - :rtype: Dict[str, Tuple[Optional[Type[DbusInterfaceBaseAsync], Dict[str, Any]]]] + :rtype: dict[str, tuple[Optional[type[DbusInterfaceBaseAsync], dict[str, Any]]]] :returns: Dictionary where keys are paths and values are tuples of managed objects classes and their properties data. *New in version 0.12.0.* diff --git a/setup.py b/setup.py index f8906ec..1ecd2a1 100644 --- a/setup.py +++ b/setup.py @@ -22,11 +22,11 @@ from os import environ from subprocess import DEVNULL, PIPE from subprocess import run as subprocess_run -from typing import List, Optional, Tuple +from typing import Optional from setuptools import Extension, setup -c_macros: List[Tuple[str, Optional[str]]] = [] +c_macros: list[tuple[str, Optional[str]]] = [] def get_libsystemd_version() -> int: @@ -55,7 +55,7 @@ def get_libsystemd_version() -> int: c_macros.append(('LIBSYSTEMD_NO_OPEN_USER_MACHINE', None)) -def get_link_arguments() -> List[str]: +def get_link_arguments() -> list[str]: process = subprocess_run( args=('pkg-config', '--libs-only-l', 'libsystemd'), stderr=DEVNULL, @@ -68,7 +68,7 @@ def get_link_arguments() -> List[str]: return result_str.rstrip(' \n').split(' ') -link_arguments: List[str] = get_link_arguments() +link_arguments: list[str] = get_link_arguments() if environ.get('PYTHON_SDBUS_USE_STATIC_LINK'): # Link statically against libsystemd and libcap @@ -77,7 +77,7 @@ def get_link_arguments() -> List[str]: link_arguments.append('-flto') -compile_arguments: List[str] = ['-flto'] +compile_arguments: list[str] = ['-flto'] use_limited_api = False diff --git a/src/sdbus/__main__.py b/src/sdbus/__main__.py index 495cdc6..a81ddc8 100644 --- a/src/sdbus/__main__.py +++ b/src/sdbus/__main__.py @@ -32,7 +32,7 @@ ) if TYPE_CHECKING: - from typing import Dict, List, Optional + from typing import Optional from .interface_generator import DbusInterfaceIntrospection @@ -41,22 +41,22 @@ class RenameMember: new_name: Optional[str] = None current_arg: Optional[str] = None - arg_renames: Dict[str, str] = field(default_factory=dict) + arg_renames: dict[str, str] = field(default_factory=dict) @dataclass class RenameInterface: new_name: Optional[str] = None current_member: Optional[RenameMember] = None - methods: Dict[str, RenameMember] = field(default_factory=dict) - properties: Dict[str, RenameMember] = field(default_factory=dict) - signals: Dict[str, RenameMember] = field(default_factory=dict) + methods: dict[str, RenameMember] = field(default_factory=dict) + properties: dict[str, RenameMember] = field(default_factory=dict) + signals: dict[str, RenameMember] = field(default_factory=dict) @dataclass class RenameRoot: current_interface: Optional[RenameInterface] = None - interfaces: Dict[str, RenameInterface] = field(default_factory=dict) + interfaces: dict[str, RenameInterface] = field(default_factory=dict) rename_root = RenameRoot() @@ -96,7 +96,7 @@ def rename_members( def rename_interfaces( - interfaces: List[DbusInterfaceIntrospection] + interfaces: list[DbusInterfaceIntrospection] ) -> None: for interface in interfaces: dbus_interface_name = interface.interface_name @@ -112,7 +112,7 @@ def rename_interfaces( def run_gen_from_connection( connection_name: str, - object_paths: List[str], + object_paths: list[str], system: bool, imports_header: bool, do_async: bool, @@ -127,7 +127,7 @@ def run_gen_from_connection( from .sd_bus_internals import sd_bus_open_system set_default_bus(sd_bus_open_system()) - interfaces: List[DbusInterfaceIntrospection] = [] + interfaces: list[DbusInterfaceIntrospection] = [] for object_path in object_paths: connection = DbusInterfaceCommon(connection_name, object_path) itrospection = connection.dbus_introspect() @@ -145,11 +145,11 @@ def run_gen_from_connection( def run_gen_from_file( - filenames: List[str], + filenames: list[str], imports_header: bool, do_async: bool, ) -> None: - interfaces: List[DbusInterfaceIntrospection] = [] + interfaces: list[DbusInterfaceIntrospection] = [] for file in filenames: interfaces.extend(interfaces_from_file(file)) @@ -308,7 +308,7 @@ def __call__( ) -def generator_main(args: Optional[List[str]] = None) -> None: +def generator_main(args: Optional[list[str]] = None) -> None: main_arg_parser = ArgumentParser( prog="sdbus", diff --git a/src/sdbus/autodoc.py b/src/sdbus/autodoc.py index 4bfafb6..8b4a918 100644 --- a/src/sdbus/autodoc.py +++ b/src/sdbus/autodoc.py @@ -28,7 +28,7 @@ from .dbus_proxy_async_signal import DbusSignalAsync if TYPE_CHECKING: - from typing import Any, Dict + from typing import Any from sphinx.application import Sphinx @@ -129,7 +129,7 @@ def add_content(self, super().add_content(*args, **kwargs) -def setup(app: Sphinx) -> Dict[str, bool]: +def setup(app: Sphinx) -> dict[str, bool]: app.setup_extension('sphinx.ext.autodoc') app.add_autodocumenter(DbusMethodDocumenter) app.add_autodocumenter(DbusPropertyDocumenter) diff --git a/src/sdbus/dbus_common_elements.py b/src/sdbus/dbus_common_elements.py index 725674e..13d2a81 100644 --- a/src/sdbus/dbus_common_elements.py +++ b/src/sdbus/dbus_common_elements.py @@ -30,17 +30,9 @@ from .sd_bus_internals import is_interface_name_valid, is_member_name_valid if TYPE_CHECKING: + from collections.abc import Callable, Sequence from types import FunctionType - from typing import ( - Any, - Callable, - Dict, - List, - Optional, - Sequence, - Tuple, - Type, - ) + from typing import Any, Optional SelfMeta = TypeVar('SelfMeta', bound="DbusInterfaceMetaCommon") @@ -63,9 +55,9 @@ class DbusMemberSync(DbusMemberCommon): class DbusInterfaceMetaCommon(type): - def __new__(cls: Type[SelfMeta], name: str, - bases: Tuple[type, ...], - namespace: Dict[str, Any], + def __new__(cls: type[SelfMeta], name: str, + bases: tuple[type, ...], + namespace: dict[str, Any], interface_name: Optional[str] = None, serving_enabled: bool = True, ) -> SelfMeta: @@ -181,7 +173,7 @@ def _rebuild_args( self, function: FunctionType, *args: Any, - **kwargs: Dict[str, Any]) -> List[Any]: + **kwargs: dict[str, Any]) -> list[Any]: # 3 types of arguments # *args - should be passed directly # **kwargs - should be put in a proper order @@ -204,7 +196,7 @@ def _rebuild_args( passed_args_iter = iter(args) default_args_iter = iter(self.args_defaults) - new_args_list: List[Any] = [] + new_args_list: list[Any] = [] for i, a_name in enumerate(self.args_spec.args[1:]): try: @@ -338,7 +330,7 @@ def __init__( class DbusLocalObjectMeta: def __init__(self) -> None: - self.activated_interfaces: List[SdBusInterface] = [] + self.activated_interfaces: list[SdBusInterface] = [] self.serving_object_path: Optional[str] = None self.attached_bus: Optional[SdBus] = None @@ -351,5 +343,5 @@ def __init__( ) -> None: self.interface_name = interface_name self.serving_enabled = serving_enabled - self.dbus_member_to_python_attr: Dict[str, str] = {} - self.python_attr_to_dbus_member: Dict[str, str] = {} + self.dbus_member_to_python_attr: dict[str, str] = {} + self.python_attr_to_dbus_member: dict[str, str] = {} diff --git a/src/sdbus/dbus_common_funcs.py b/src/sdbus/dbus_common_funcs.py index 7e7baf7..f7122b2 100644 --- a/src/sdbus/dbus_common_funcs.py +++ b/src/sdbus/dbus_common_funcs.py @@ -37,7 +37,8 @@ ) if TYPE_CHECKING: - from typing import Any, Dict, Generator, Iterator, Literal, Mapping, Tuple + from collections.abc import Generator, Iterator, Mapping + from typing import Any, Literal from .sd_bus_internals import SdBus @@ -167,11 +168,11 @@ def _check_sync_in_async_env() -> bool: def _parse_properties_vardict( properties_name_map: Mapping[str, str], - properties_vardict: Dict[str, Tuple[str, Any]], + properties_vardict: dict[str, tuple[str, Any]], on_unknown_member: Literal['error', 'ignore', 'reuse'], -) -> Dict[str, Any]: +) -> dict[str, Any]: - properties_translated: Dict[str, Any] = {} + properties_translated: dict[str, Any] = {} for member_name, variant in properties_vardict.items(): try: diff --git a/src/sdbus/dbus_exceptions.py b/src/sdbus/dbus_exceptions.py index b5d4544..eb78b99 100644 --- a/src/sdbus/dbus_exceptions.py +++ b/src/sdbus/dbus_exceptions.py @@ -28,7 +28,7 @@ ) if TYPE_CHECKING: - from typing import Any, Dict, Tuple + from typing import Any class DbusErrorMeta(type): @@ -36,8 +36,8 @@ class DbusErrorMeta(type): def __new__( cls, name: str, - bases: Tuple[type, ...], - namespace: Dict[str, Any], + bases: tuple[type, ...], + namespace: dict[str, Any], ) -> DbusErrorMeta: 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 9f48121..6beb8d1 100644 --- a/src/sdbus/dbus_proxy_async_interface_base.py +++ b/src/sdbus/dbus_proxy_async_interface_base.py @@ -19,11 +19,12 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA from __future__ import annotations +from collections.abc import Callable from copy import copy from inspect import getmembers from itertools import chain from types import MethodType -from typing import TYPE_CHECKING, Any, Callable, cast +from typing import TYPE_CHECKING, Any, cast from warnings import warn from weakref import WeakKeyDictionary, WeakValueDictionary @@ -48,18 +49,8 @@ from .sd_bus_internals import SdBusInterface if TYPE_CHECKING: - from typing import ( - Dict, - Iterable, - Iterator, - List, - Optional, - Set, - Tuple, - Type, - TypeVar, - Union, - ) + from collections.abc import Iterable, Iterator + from typing import Optional, TypeVar, Union from .dbus_common_elements import DbusBoundAsync from .sd_bus_internals import SdBus, SdBusSlot @@ -81,7 +72,7 @@ class DbusInterfaceMetaAsync(DbusInterfaceMetaCommon): def _process_dbus_method_override( override_attr_name: str, override: DbusMethodOverride[T], - mro_dbus_elements: Dict[str, DbusMemberAsync], + mro_dbus_elements: dict[str, DbusMemberAsync], ) -> DbusMethodAsync: try: original_method = mro_dbus_elements[override_attr_name] @@ -105,7 +96,7 @@ def _process_dbus_method_override( def _process_dbus_property_override( override_attr_name: str, override: DbusPropertyOverride[T], - mro_dbus_elements: Dict[str, DbusMemberAsync], + mro_dbus_elements: dict[str, DbusMemberAsync], ) -> DbusPropertyAsync[Any]: try: original_property = mro_dbus_elements[override_attr_name] @@ -136,12 +127,12 @@ def _process_dbus_property_override( def _check_collisions( cls, new_class_name: str, - namespace: Dict[str, Any], - mro_dbus_elements: Dict[str, DbusMemberAsync], + namespace: dict[str, Any], + mro_dbus_elements: dict[str, DbusMemberAsync], ) -> None: possible_collisions = namespace.keys() & mro_dbus_elements.keys() - new_overrides: Dict[str, DbusMemberAsync] = {} + new_overrides: dict[str, DbusMemberAsync] = {} for attr_name, attr in namespace.items(): if isinstance(attr, DbusMethodOverride): @@ -173,8 +164,8 @@ def _check_collisions( def _extract_dbus_elements( dbus_class: type, dbus_meta: DbusClassMeta, - ) -> Dict[str, DbusMemberAsync]: - dbus_elements_map: Dict[str, DbusMemberAsync] = {} + ) -> dict[str, DbusMemberAsync]: + dbus_elements_map: dict[str, DbusMemberAsync] = {} for attr_name in dbus_meta.python_attr_to_dbus_member.keys(): dbus_element = dbus_class.__dict__.get(attr_name) @@ -193,9 +184,9 @@ def _map_mro_dbus_elements( cls, new_class_name: str, base_classes: Iterable[type], - ) -> Dict[str, DbusMemberAsync]: - all_python_dbus_map: Dict[str, DbusMemberAsync] = {} - possible_collisions: Set[str] = set() + ) -> dict[str, DbusMemberAsync]: + all_python_dbus_map: dict[str, DbusMemberAsync] = {} + possible_collisions: set[str] = set() for c in base_classes: dbus_meta = DBUS_CLASS_TO_META.get(c) @@ -252,8 +243,8 @@ def _map_dbus_elements( raise TypeError(f"Unknown D-Bus element: {attr!r}") def __new__(cls, name: str, - bases: Tuple[type, ...], - namespace: Dict[str, Any], + bases: tuple[type, ...], + namespace: dict[str, Any], interface_name: Optional[str] = None, serving_enabled: bool = True, ) -> DbusInterfaceMetaAsync: @@ -264,7 +255,7 @@ def __new__(cls, name: str, "already created." ) - all_mro_bases: Set[Type[Any]] = set( + all_mro_bases: set[type[Any]] = set( chain.from_iterable((c.__mro__ for c in bases)) ) reserved_dbus_map = cls._map_mro_dbus_elements( @@ -303,7 +294,7 @@ def __init__(self) -> None: @classmethod def _dbus_iter_interfaces_meta( cls, - ) -> Iterator[Tuple[str, DbusClassMeta]]: + ) -> Iterator[tuple[str, DbusClassMeta]]: for base in cls.__mro__: meta = DBUS_CLASS_TO_META.get(base) @@ -344,7 +335,7 @@ def export_to_dbus( local_object_meta.attached_bus = bus local_object_meta.serving_object_path = object_path # TODO: can be optimized with a single loop - interface_map: Dict[str, List[DbusBoundAsync]] = {} + interface_map: dict[str, list[DbusBoundAsync]] = {} for key, value in getmembers(self): assert not isinstance(value, DbusMemberAsync) @@ -448,7 +439,7 @@ def _proxify( @classmethod def new_connect( - cls: Type[Self], + cls: type[Self], service_name: str, object_path: str, bus: Optional[SdBus] = None, @@ -464,7 +455,7 @@ def new_connect( @classmethod def new_proxy( - cls: Type[Self], + cls: type[Self], service_name: str, object_path: str, bus: Optional[SdBus] = None, @@ -477,7 +468,7 @@ def new_proxy( class DbusExportHandle: def __init__(self, local_meta: DbusLocalObjectMeta): - self._dbus_slots: List[SdBusSlot] = [ + self._dbus_slots: list[SdBusSlot] = [ i.slot for i in local_meta.activated_interfaces if i.slot is not None diff --git a/src/sdbus/dbus_proxy_async_interfaces.py b/src/sdbus/dbus_proxy_async_interfaces.py index ab8a1a6..7964b0b 100644 --- a/src/sdbus/dbus_proxy_async_interfaces.py +++ b/src/sdbus/dbus_proxy_async_interfaces.py @@ -27,13 +27,13 @@ from .dbus_proxy_async_signal import dbus_signal_async if TYPE_CHECKING: - from typing import Any, Dict, List, Literal, Tuple + from typing import Any, Literal DBUS_PROPERTIES_CHANGED_TYPING = ( - Tuple[ + tuple[ str, - Dict[str, Tuple[str, Any]], - List[str], + dict[str, tuple[str, Any]], + list[str], ] ) @@ -76,15 +76,15 @@ def properties_changed(self) -> DBUS_PROPERTIES_CHANGED_TYPING: @dbus_method_async('s', 'a{sv}', method_name='GetAll') async def _properties_get_all( - self, interface_name: str) -> Dict[str, Tuple[str, Any]]: + self, interface_name: str) -> dict[str, tuple[str, Any]]: raise NotImplementedError async def properties_get_all_dict( self, on_unknown_member: Literal['error', 'ignore', 'reuse'] = 'error', - ) -> Dict[str, Any]: + ) -> dict[str, Any]: - properties: Dict[str, Any] = {} + properties: dict[str, Any] = {} for interface_name, meta in self._dbus_iter_interfaces_meta(): if not meta.serving_enabled: diff --git a/src/sdbus/dbus_proxy_async_method.py b/src/sdbus/dbus_proxy_async_method.py index d671df6..b05ea1d 100644 --- a/src/sdbus/dbus_proxy_async_method.py +++ b/src/sdbus/dbus_proxy_async_method.py @@ -36,7 +36,8 @@ from .sd_bus_internals import DbusNoReplyFlag if TYPE_CHECKING: - from typing import Any, Callable, Optional, Sequence, Type, TypeVar, Union + from collections.abc import Callable, Sequence + from typing import Any, Optional, TypeVar, Union from .dbus_proxy_async_interface_base import DbusInterfaceBaseAsync from .sd_bus_internals import SdBusMessage @@ -58,7 +59,7 @@ class DbusMethodAsync(DbusMethodCommon, DbusMemberAsync): def __get__( self, obj: None, - obj_class: Type[DbusInterfaceBaseAsync], + obj_class: type[DbusInterfaceBaseAsync], ) -> DbusMethodAsync: ... @@ -66,14 +67,14 @@ def __get__( def __get__( self, obj: DbusInterfaceBaseAsync, - obj_class: Type[DbusInterfaceBaseAsync], + obj_class: type[DbusInterfaceBaseAsync], ) -> Callable[..., Any]: ... def __get__( self, obj: Optional[DbusInterfaceBaseAsync], - obj_class: Optional[Type[DbusInterfaceBaseAsync]] = None, + obj_class: Optional[type[DbusInterfaceBaseAsync]] = None, ) -> Union[Callable[..., Any], DbusMethodAsync]: if obj is not None: dbus_meta = obj._dbus diff --git a/src/sdbus/dbus_proxy_async_object_manager.py b/src/sdbus/dbus_proxy_async_object_manager.py index 33f86b1..584087e 100644 --- a/src/sdbus/dbus_proxy_async_object_manager.py +++ b/src/sdbus/dbus_proxy_async_object_manager.py @@ -33,7 +33,8 @@ from .dbus_proxy_async_signal import dbus_signal_async if TYPE_CHECKING: - from typing import Any, Callable, Dict, List, Optional, Tuple + from collections.abc import Callable + from typing import Any, Optional from .sd_bus_internals import SdBus, SdBusSlot @@ -60,19 +61,19 @@ class DbusObjectManagerInterfaceAsync( def __init__(self) -> None: super().__init__() self._object_manager_slot: Optional[SdBusSlot] = None - self._managed_object_to_path: Dict[DbusInterfaceBaseAsync, str] = {} + self._managed_object_to_path: dict[DbusInterfaceBaseAsync, str] = {} @dbus_method_async(result_signature='a{oa{sa{sv}}}') async def get_managed_objects( - self) -> Dict[str, Dict[str, Dict[str, Any]]]: + self) -> dict[str, dict[str, dict[str, Any]]]: raise NotImplementedError @dbus_signal_async('oa{sa{sv}}') - def interfaces_added(self) -> Tuple[str, Dict[str, Dict[str, Any]]]: + def interfaces_added(self) -> tuple[str, dict[str, dict[str, Any]]]: raise NotImplementedError @dbus_signal_async('oao') - def interfaces_removed(self) -> Tuple[str, List[str]]: + def interfaces_removed(self) -> tuple[str, list[str]]: raise NotImplementedError def export_to_dbus( diff --git a/src/sdbus/dbus_proxy_async_property.py b/src/sdbus/dbus_proxy_async_property.py index 9cac940..bf63f04 100644 --- a/src/sdbus/dbus_proxy_async_property.py +++ b/src/sdbus/dbus_proxy_async_property.py @@ -19,9 +19,10 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA from __future__ import annotations +from collections.abc import Awaitable from inspect import iscoroutinefunction from types import FunctionType -from typing import TYPE_CHECKING, Awaitable, Generic, TypeVar, cast, overload +from typing import TYPE_CHECKING, Generic, TypeVar, cast, overload from weakref import ref as weak_ref from .dbus_common_elements import ( @@ -33,7 +34,8 @@ ) if TYPE_CHECKING: - from typing import Any, Callable, Generator, Optional, Type, Union + from collections.abc import Callable, Generator + from typing import Any, Optional, Union from .dbus_proxy_async_interface_base import DbusInterfaceBaseAsync from .sd_bus_internals import SdBusMessage @@ -75,7 +77,7 @@ def __init__( def __get__( self, obj: None, - obj_class: Type[DbusInterfaceBaseAsync], + obj_class: type[DbusInterfaceBaseAsync], ) -> DbusPropertyAsync[T]: ... @@ -83,14 +85,14 @@ def __get__( def __get__( self, obj: DbusInterfaceBaseAsync, - obj_class: Type[DbusInterfaceBaseAsync], + obj_class: type[DbusInterfaceBaseAsync], ) -> DbusBoundPropertyAsyncBase[T]: ... def __get__( self, obj: Optional[DbusInterfaceBaseAsync], - obj_class: Optional[Type[DbusInterfaceBaseAsync]] = None, + obj_class: Optional[type[DbusInterfaceBaseAsync]] = None, ) -> Union[DbusBoundPropertyAsyncBase[T], DbusPropertyAsync[T]]: if obj is not None: dbus_meta = obj._dbus diff --git a/src/sdbus/dbus_proxy_async_signal.py b/src/sdbus/dbus_proxy_async_signal.py index 8de4c25..b425d49 100644 --- a/src/sdbus/dbus_proxy_async_signal.py +++ b/src/sdbus/dbus_proxy_async_signal.py @@ -20,17 +20,10 @@ from __future__ import annotations from asyncio import Queue +from collections.abc import AsyncIterable, AsyncIterator from contextlib import closing from types import FunctionType -from typing import ( - TYPE_CHECKING, - AsyncIterable, - AsyncIterator, - Generic, - TypeVar, - cast, - overload, -) +from typing import TYPE_CHECKING, Generic, TypeVar, cast, overload from weakref import WeakSet from .dbus_common_elements import ( @@ -43,7 +36,8 @@ from .dbus_common_funcs import get_default_bus if TYPE_CHECKING: - from typing import Any, Callable, Optional, Sequence, Tuple, Type, Union + from collections.abc import Callable, Sequence + from typing import Any, Optional, Union from .dbus_proxy_async_interface_base import DbusInterfaceBaseAsync from .sd_bus_internals import SdBus, SdBusMessage, SdBusSlot @@ -76,7 +70,7 @@ def __init__( def __get__( self, obj: None, - obj_class: Type[DbusInterfaceBaseAsync], + obj_class: type[DbusInterfaceBaseAsync], ) -> DbusSignalAsync[T]: ... @@ -84,14 +78,14 @@ def __get__( def __get__( self, obj: DbusInterfaceBaseAsync, - obj_class: Type[DbusInterfaceBaseAsync], + obj_class: type[DbusInterfaceBaseAsync], ) -> DbusBoundSignalAsyncBase[T]: ... def __get__( self, obj: Optional[DbusInterfaceBaseAsync], - obj_class: Optional[Type[DbusInterfaceBaseAsync]] = None, + obj_class: Optional[type[DbusInterfaceBaseAsync]] = None, ) -> Union[DbusBoundSignalAsyncBase[T], DbusSignalAsync[T]]: if obj is not None: dbus_meta = obj._dbus @@ -106,7 +100,7 @@ async def catch_anywhere( self, service_name: str, bus: Optional[SdBus] = None, - ) -> AsyncIterable[Tuple[str, T]]: + ) -> AsyncIterable[tuple[str, T]]: if bus is None: bus = get_default_bus() @@ -142,7 +136,7 @@ async def catch_anywhere( self, service_name: Optional[str] = None, bus: Optional[SdBus] = None, - ) -> AsyncIterable[Tuple[str, T]]: + ) -> AsyncIterable[tuple[str, T]]: raise NotImplementedError yield "", cast(T, None) @@ -193,7 +187,7 @@ async def catch_anywhere( self, service_name: Optional[str] = None, bus: Optional[SdBus] = None, - ) -> AsyncIterable[Tuple[str, T]]: + ) -> AsyncIterable[tuple[str, T]]: if bus is None: bus = self.proxy_meta.attached_bus @@ -254,7 +248,7 @@ async def catch_anywhere( self, service_name: Optional[str] = None, bus: Optional[SdBus] = None, - ) -> AsyncIterable[Tuple[str, T]]: + ) -> AsyncIterable[tuple[str, T]]: raise NotImplementedError("TODO") yield diff --git a/src/sdbus/dbus_proxy_sync_interface_base.py b/src/sdbus/dbus_proxy_sync_interface_base.py index 783256e..0290bb4 100644 --- a/src/sdbus/dbus_proxy_sync_interface_base.py +++ b/src/sdbus/dbus_proxy_sync_interface_base.py @@ -34,16 +34,8 @@ from .dbus_proxy_sync_property import DbusPropertySync if TYPE_CHECKING: - from typing import ( - Any, - Dict, - Iterable, - Iterator, - Optional, - Set, - Tuple, - Type, - ) + from collections.abc import Iterable, Iterator + from typing import Any, Optional from .sd_bus_internals import SdBus @@ -59,8 +51,8 @@ class DbusInterfaceMetaSync(DbusInterfaceMetaCommon): @staticmethod def _check_collisions( new_class_name: str, - attr_names: Set[str], - reserved_attr_names: Set[str], + attr_names: set[str], + reserved_attr_names: set[str], ) -> None: possible_collisions = attr_names & reserved_attr_names @@ -74,9 +66,9 @@ def _check_collisions( def _collect_dbus_to_python_attr_names( new_class_name: str, base_classes: Iterable[type], - ) -> Set[str]: - all_python_dbus_attrs: Set[str] = set() - possible_collisions: Set[str] = set() + ) -> set[str]: + all_python_dbus_attrs: set[str] = set() + possible_collisions: set[str] = set() for c in base_classes: dbus_meta = DBUS_CLASS_TO_META.get(c) @@ -127,8 +119,8 @@ def _map_dbus_elements( raise TypeError(f"Unknown D-Bus element: {attr!r}") def __new__(cls, name: str, - bases: Tuple[type, ...], - namespace: Dict[str, Any], + bases: tuple[type, ...], + namespace: dict[str, Any], interface_name: Optional[str] = None, serving_enabled: bool = True, ) -> DbusInterfaceMetaSync: @@ -139,7 +131,7 @@ def __new__(cls, name: str, "already created." ) - all_mro_bases: Set[Type[Any]] = set( + all_mro_bases: set[type[Any]] = set( chain.from_iterable((c.__mro__ for c in bases)) ) reserved_attr_names = cls._collect_dbus_to_python_attr_names( @@ -177,7 +169,7 @@ def __init__( @classmethod def _dbus_iter_interfaces_meta( cls, - ) -> Iterator[Tuple[str, DbusClassMeta]]: + ) -> Iterator[tuple[str, DbusClassMeta]]: for base in cls.__mro__: meta = DBUS_CLASS_TO_META.get(base) diff --git a/src/sdbus/dbus_proxy_sync_interfaces.py b/src/sdbus/dbus_proxy_sync_interfaces.py index 066891c..90cc21f 100644 --- a/src/sdbus/dbus_proxy_sync_interfaces.py +++ b/src/sdbus/dbus_proxy_sync_interfaces.py @@ -25,7 +25,7 @@ from .dbus_proxy_sync_method import dbus_method if TYPE_CHECKING: - from typing import Any, Dict, Literal, Tuple + from typing import Any, Literal class DbusPeerInterface( @@ -61,14 +61,14 @@ class DbusPropertiesInterface( ): @dbus_method('s', 'a{sv}', method_name='GetAll') def _properties_get_all( - self, interface_name: str) -> Dict[str, Tuple[str, Any]]: + self, interface_name: str) -> dict[str, tuple[str, Any]]: raise NotImplementedError def properties_get_all_dict( self, on_unknown_member: Literal['error', 'ignore', 'reuse'] = 'error', - ) -> Dict[str, Any]: - properties: Dict[str, Any] = {} + ) -> dict[str, Any]: + properties: dict[str, Any] = {} for interface_name, meta in self._dbus_iter_interfaces_meta(): if not meta.serving_enabled: @@ -107,5 +107,5 @@ class DbusObjectManagerInterface( ): @dbus_method(result_signature='a{oa{sa{sv}}}') def get_managed_objects( - self) -> Dict[str, Dict[str, Dict[str, Any]]]: + self) -> dict[str, dict[str, dict[str, Any]]]: raise NotImplementedError diff --git a/src/sdbus/dbus_proxy_sync_method.py b/src/sdbus/dbus_proxy_sync_method.py index 82e2d71..0ba58e1 100644 --- a/src/sdbus/dbus_proxy_sync_method.py +++ b/src/sdbus/dbus_proxy_sync_method.py @@ -30,7 +30,8 @@ ) if TYPE_CHECKING: - from typing import Any, Callable, Optional, Sequence, Type + from collections.abc import Callable, Sequence + from typing import Any, Optional from .dbus_proxy_sync_interface_base import DbusInterfaceBase @@ -40,7 +41,7 @@ class DbusMethodSync(DbusMethodCommon, DbusMemberSync): def __get__(self, obj: DbusInterfaceBase, - obj_class: Optional[Type[DbusInterfaceBase]] = None, + obj_class: Optional[type[DbusInterfaceBase]] = None, ) -> Callable[..., Any]: return DbusLocalMethodSync(self, obj) diff --git a/src/sdbus/dbus_proxy_sync_property.py b/src/sdbus/dbus_proxy_sync_property.py index 4cbb5af..20e79a1 100644 --- a/src/sdbus/dbus_proxy_sync_property.py +++ b/src/sdbus/dbus_proxy_sync_property.py @@ -27,7 +27,8 @@ from .dbus_common_funcs import _check_sync_in_async_env if TYPE_CHECKING: - from typing import Any, Callable, Optional, Type + from collections.abc import Callable + from typing import Any, Optional from .dbus_proxy_sync_interface_base import DbusInterfaceBase @@ -62,7 +63,7 @@ def __init__( def __get__(self, obj: DbusInterfaceBase, - obj_class: Optional[Type[DbusInterfaceBase]] = None, + obj_class: Optional[type[DbusInterfaceBase]] = None, ) -> T: assert _check_sync_in_async_env(), ( "Used sync __get__ method in async environment. " diff --git a/src/sdbus/interface_generator.py b/src/sdbus/interface_generator.py index e7c3f9c..1272d81 100644 --- a/src/sdbus/interface_generator.py +++ b/src/sdbus/interface_generator.py @@ -25,16 +25,8 @@ from xml.etree.ElementTree import parse as etree_from_file if TYPE_CHECKING: - from typing import ( - Dict, - Iterable, - Iterator, - List, - Literal, - Optional, - Tuple, - Union, - ) + from collections.abc import Iterable, Iterator + from typing import Literal, Optional, Union from xml.etree.ElementTree import Element @@ -146,7 +138,7 @@ def typing_into_tuple(typing_iter: Iterable[str]) -> str: @staticmethod def slice_container(dbus_sig_iter: Iterator[str], peek_str: str) -> str: - accumulator: List[str] = [peek_str] + accumulator: list[str] = [peek_str] round_braces_count = 0 curly_braces_count = 0 @@ -186,8 +178,8 @@ def slice_container(dbus_sig_iter: Iterator[str], peek_str: str) -> str: return ''.join(accumulator) @classmethod - def split_sig(cls, sig: str) -> List[str]: - completes: List[str] = [] + def split_sig(cls, sig: str) -> list[str]: + completes: list[str] = [] sig_iter = iter(sig) @@ -249,7 +241,7 @@ def typing_complete(cls, complete_sig: str) -> str: return cls.typing_basic(complete_sig) @classmethod - def result_typing(cls, result_args: List[str]) -> str: + def result_typing(cls, result_args: list[str]) -> str: result_len = len(result_args) if result_len == 0: @@ -365,8 +357,8 @@ def __init__(self, element: Element): self.is_no_reply = False - self.input_args: List[DbusArgsIntrospection] = [] - self.result_args: List[DbusArgsIntrospection] = [] + self.input_args: list[DbusArgsIntrospection] = [] + self.result_args: list[DbusArgsIntrospection] = [] super().__init__(element) @@ -399,8 +391,8 @@ def dbus_result_signature(self) -> str: ) @property - def args_names_and_typing(self) -> List[Tuple[str, str]]: - arg_names: List[Tuple[str, str]] = [] + def args_names_and_typing(self) -> list[tuple[str, str]]: + arg_names: list[tuple[str, str]] = [] for i, input_arg in enumerate(self.input_args): if input_arg.name is not None: @@ -432,7 +424,7 @@ def __repr__(self) -> str: class DbusPropertyIntrospection(DbusMemberAbstract): - _EMITS_CHANGED_MAP: Dict[ + _EMITS_CHANGED_MAP: dict[ Union[bool, Literal['const', 'invalidates']], str ] = { True: 'DbusPropertyEmitsChangeFlag', @@ -504,7 +496,7 @@ def __init__(self, element: Element): if element.tag != 'signal': raise ValueError(f"Expected signal tag, got {element.tag}") - self.args: List[DbusArgsIntrospection] = [] + self.args: list[DbusArgsIntrospection] = [] super().__init__(element) def _can_use_unpivileged(self) -> bool: @@ -548,9 +540,9 @@ def __init__(self, element: Element): self.is_deprecated = False self.c_name: Optional[str] = None - self.methods: List[DbusMethodInrospection] = [] - self.properties: List[DbusPropertyIntrospection] = [] - self.signals: List[DbusSignalIntrospection] = [] + self.methods: list[DbusMethodInrospection] = [] + self.properties: list[DbusPropertyIntrospection] = [] + self.signals: list[DbusSignalIntrospection] = [] for dbus_member in element: if dbus_member.tag == 'method': self.methods.append(DbusMethodInrospection(dbus_member)) @@ -584,7 +576,7 @@ def has_members(self) -> bool: } -INTERFACE_TEMPLATES: Dict[str, str] = { +INTERFACE_TEMPLATES: dict[str, str] = { "generic_no_members": """\ ... # Interface has no members """, @@ -809,9 +801,9 @@ def {{ a_property.python_name }}(self) -> {{ a_property.typing }}: def xml_to_interfaces_introspection( - root: Element) -> List[DbusInterfaceIntrospection]: + root: Element) -> list[DbusInterfaceIntrospection]: - list_of_interface_introspection: List[DbusInterfaceIntrospection] = [] + list_of_interface_introspection: list[DbusInterfaceIntrospection] = [] if root.tag != 'node': raise ValueError(f"Expected node tag got {root.tag}") @@ -830,14 +822,14 @@ def xml_to_interfaces_introspection( def interfaces_from_file(filename_or_path: Union[str, Path] - ) -> List[DbusInterfaceIntrospection]: + ) -> list[DbusInterfaceIntrospection]: etree = etree_from_file(filename_or_path) return xml_to_interfaces_introspection(etree.getroot()) -def interfaces_from_str(xml_str: str) -> List[DbusInterfaceIntrospection]: +def interfaces_from_str(xml_str: str) -> list[DbusInterfaceIntrospection]: etree = etree_from_str(xml_str) @@ -845,7 +837,7 @@ def interfaces_from_str(xml_str: str) -> List[DbusInterfaceIntrospection]: def generate_py_file( - interfaces: List[DbusInterfaceIntrospection], + interfaces: list[DbusInterfaceIntrospection], include_import_header: bool = True, do_async: bool = True, ) -> str: diff --git a/src/sdbus/sd_bus_internals.py b/src/sdbus/sd_bus_internals.py index 699455f..d6746dd 100644 --- a/src/sdbus/sd_bus_internals.py +++ b/src/sdbus/sd_bus_internals.py @@ -23,24 +23,14 @@ from typing import TYPE_CHECKING if TYPE_CHECKING: - from typing import ( - Any, - Callable, - Coroutine, - Dict, - List, - Optional, - Sequence, - Tuple, - Type, - Union, - ) + from collections.abc import Callable, Coroutine, Sequence + from typing import Any, Optional, Union DbusBasicTypes = Union[str, int, bytes, float, Any] - DbusStructType = Tuple[DbusBasicTypes, ...] - DbusDictType = Dict[DbusBasicTypes, DbusBasicTypes] - DbusVariantType = Tuple[str, DbusStructType] - DbusListType = List[DbusBasicTypes] + DbusStructType = tuple[DbusBasicTypes, ...] + DbusDictType = dict[DbusBasicTypes, DbusBasicTypes] + DbusVariantType = tuple[str, DbusStructType] + DbusListType = list[DbusBasicTypes] DbusCompleteTypes = Union[DbusBasicTypes, DbusStructType, DbusDictType, DbusVariantType, DbusListType] @@ -60,12 +50,12 @@ def close(self) -> None: class SdBusInterface: slot: Optional[SdBusSlot] - method_list: List[object] - method_dict: Dict[bytes, object] - property_list: List[object] - property_get_dict: Dict[bytes, object] - property_set_dict: Dict[bytes, object] - signal_list: List[object] + method_list: list[object] + method_dict: dict[bytes, object] + property_list: list[object] + property_get_dict: dict[bytes, object] + property_set_dict: dict[bytes, object] + signal_list: list[object] def add_method( self, @@ -122,7 +112,7 @@ def seal(self) -> None: raise NotImplementedError(__STUB_ERROR) def get_contents(self - ) -> Tuple[DbusCompleteTypes, ...]: + ) -> tuple[DbusCompleteTypes, ...]: raise NotImplementedError(__STUB_ERROR) def create_reply(self) -> SdBusMessage: @@ -137,7 +127,7 @@ def create_error_reply( def send(self) -> None: raise NotImplementedError(__STUB_ERROR) - def parse_to_tuple(self) -> Tuple[Any, ...]: + def parse_to_tuple(self) -> tuple[Any, ...]: raise NotImplementedError(__STUB_ERROR) expect_reply: bool = False @@ -261,7 +251,7 @@ def decode_object_path(prefix: str, full_path: str) -> str: raise NotImplementedError(__STUB_ERROR) -def map_exception_to_dbus_error(exc: Type[Exception], +def map_exception_to_dbus_error(exc: type[Exception], dbus_error_name: str, /) -> None: ... # We want to be able to generate docs without module @@ -314,9 +304,9 @@ class SdBusRequestNameAlreadyOwnerError(SdBusRequestNameError): ... -DBUS_ERROR_TO_EXCEPTION: Dict[str, Exception] = {} +DBUS_ERROR_TO_EXCEPTION: dict[str, Exception] = {} -EXCEPTION_TO_DBUS_ERROR: Dict[Exception, str] = {} +EXCEPTION_TO_DBUS_ERROR: dict[Exception, str] = {} DbusDeprecatedFlag: int = 0 DbusHiddenFlag: int = 0 diff --git a/src/sdbus/unittest.py b/src/sdbus/unittest.py index 8b9a2ba..b31a083 100644 --- a/src/sdbus/unittest.py +++ b/src/sdbus/unittest.py @@ -37,15 +37,9 @@ from .sd_bus_internals import SdBusMessage, sd_bus_open_user if TYPE_CHECKING: - from typing import ( - Any, - AsyncContextManager, - Iterator, - List, - Optional, - TypeVar, - Union, - ) + from collections.abc import Iterator + from contextlib import AbstractAsyncContextManager + from typing import Any, Optional, TypeVar, Union from .dbus_proxy_async_signal import ( DbusBoundSignalAsyncBase, @@ -77,7 +71,7 @@ def __init__( timeout: Union[int, float], ): self._timeout = timeout - self._captured_data: List[Any] = [] + self._captured_data: list[Any] = [] self._ready_event = Event() self._callback_method = self._callback @@ -112,7 +106,7 @@ def _callback(self, data: Any) -> None: self._ready_event.set() @property - def output(self) -> List[Any]: + def output(self) -> list[Any]: return self._captured_data.copy() @@ -239,7 +233,7 @@ def assertDbusSignalEmits( self, signal: DbusBoundSignalAsyncBase[Any], timeout: Union[int, float] = 1, - ) -> AsyncContextManager[DbusSignalRecorderBase]: + ) -> AbstractAsyncContextManager[DbusSignalRecorderBase]: if isinstance(signal, DbusLocalSignalAsync): return DbusSignalRecorderLocal(timeout, signal) diff --git a/src/sdbus/utils/parse.py b/src/sdbus/utils/parse.py index f2b749d..de09386 100644 --- a/src/sdbus/utils/parse.py +++ b/src/sdbus/utils/parse.py @@ -29,35 +29,25 @@ ) if TYPE_CHECKING: - from typing import ( - Any, - Dict, - FrozenSet, - Iterable, - List, - Literal, - Optional, - Tuple, - Type, - Union, - ) + from collections.abc import Iterable + from typing import Any, Literal, Optional, Union from ..dbus_proxy_async_interfaces import DBUS_PROPERTIES_CHANGED_TYPING InterfacesInputElements = Union[ DbusInterfaceBaseAsync, - Type[DbusInterfaceBaseAsync], + type[DbusInterfaceBaseAsync], ] InterfacesInput = Union[ InterfacesInputElements, Iterable[InterfacesInputElements], ] - InterfacesToClassMap = Dict[FrozenSet[str], Type[DbusInterfaceBaseAsync]] + InterfacesToClassMap = dict[frozenset[str], type[DbusInterfaceBaseAsync]] OnUnknownMember = Literal['error', 'ignore', 'reuse'] OnUnknownInterface = Literal['error', 'none'] - ParseGetManaged = Dict[ + ParseGetManaged = dict[ str, - Tuple[Optional[Type[DbusInterfaceBaseAsync]], Dict[str, Any]], + tuple[Optional[type[DbusInterfaceBaseAsync]], dict[str, Any]], ] @@ -65,7 +55,7 @@ def parse_properties_changed( interface: InterfacesInputElements, properties_changed_data: DBUS_PROPERTIES_CHANGED_TYPING, on_unknown_member: OnUnknownMember = 'error', -) -> Dict[str, Any]: +) -> dict[str, Any]: interface_name, changed_properties, invalidated_properties = ( properties_changed_data ) @@ -120,7 +110,7 @@ def _get_class_from_interfaces( interfaces_to_class_map: InterfacesToClassMap, interface_names_iter: Iterable[str], raise_key_error: bool, -) -> Optional[Type[DbusInterfaceBaseAsync]]: +) -> Optional[type[DbusInterfaceBaseAsync]]: class_set = frozenset(interface_names_iter) - SKIP_INTERFACES try: return interfaces_to_class_map[class_set] @@ -132,8 +122,8 @@ def _get_class_from_interfaces( def _get_member_map_from_class( - python_class: Optional[Type[DbusInterfaceBaseAsync]], -) -> Dict[str, Dict[str, str]]: + python_class: Optional[type[DbusInterfaceBaseAsync]], +) -> dict[str, dict[str, str]]: if python_class is None: return {} else: @@ -145,11 +135,11 @@ def _get_member_map_from_class( def _translate_and_merge_members( - properties_data: Dict[str, Dict[str, Any]], - dbus_to_python_map: Dict[str, Dict[str, str]], + properties_data: dict[str, dict[str, Any]], + dbus_to_python_map: dict[str, dict[str, str]], on_unknown_member: OnUnknownMember, -) -> Dict[str, Any]: - python_properties: Dict[str, Any] = {} +) -> dict[str, Any]: + python_properties: dict[str, Any] = {} for interface_name, properties in properties_data.items(): interface_member_map = dbus_to_python_map.get( interface_name, {}, @@ -167,10 +157,10 @@ def _translate_and_merge_members( def parse_interfaces_added( interfaces: InterfacesInput, - interfaces_added_data: Tuple[str, Dict[str, Dict[str, Any]]], + interfaces_added_data: tuple[str, dict[str, dict[str, Any]]], on_unknown_interface: OnUnknownInterface = 'error', on_unknown_member: OnUnknownMember = 'error', -) -> Tuple[str, Optional[Type[DbusInterfaceBaseAsync]], Dict[str, Any]]: +) -> tuple[str, Optional[type[DbusInterfaceBaseAsync]], dict[str, Any]]: interfaces_to_class_map = _create_interfaces_map(interfaces) @@ -184,7 +174,7 @@ def parse_interfaces_added( ) ) dbus_to_python_member_map = _get_member_map_from_class(python_class) - python_properties: Dict[str, Any] = {} + python_properties: dict[str, Any] = {} for interface_name, properties in properties_data.items(): interface_member_map = dbus_to_python_member_map.get( interface_name, {}, @@ -210,9 +200,9 @@ def parse_interfaces_added( def parse_interfaces_removed( interfaces: InterfacesInput, - interfaces_removed_data: Tuple[str, List[str]], + interfaces_removed_data: tuple[str, list[str]], on_unknown_interface: OnUnknownInterface = 'error', -) -> Tuple[str, Optional[Type[DbusInterfaceBaseAsync]]]: +) -> tuple[str, Optional[type[DbusInterfaceBaseAsync]]]: interfaces_to_class_map = _create_interfaces_map(interfaces) @@ -231,7 +221,7 @@ def parse_interfaces_removed( def parse_get_managed_objects( interfaces: InterfacesInput, - managed_objects_data: Dict[str, Dict[str, Dict[str, Any]]], + managed_objects_data: dict[str, dict[str, dict[str, Any]]], on_unknown_interface: OnUnknownInterface = 'error', on_unknown_member: OnUnknownMember = 'error', ) -> ParseGetManaged: diff --git a/src/sdbus_async/dbus_daemon/__init__.py b/src/sdbus_async/dbus_daemon/__init__.py index 69c177e..e813417 100644 --- a/src/sdbus_async/dbus_daemon/__init__.py +++ b/src/sdbus_async/dbus_daemon/__init__.py @@ -19,7 +19,7 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA from __future__ import annotations -from typing import List, Optional, Tuple +from typing import Optional from sdbus import ( DbusInterfaceCommonAsync, @@ -91,7 +91,7 @@ async def get_name_owner(self, service_name: str) -> str: raise NotImplementedError @dbus_method_async() - async def list_activatable_names(self) -> List[str]: + async def list_activatable_names(self) -> list[str]: """Lists all activatable services names. :return: List of all names. @@ -99,7 +99,7 @@ async def list_activatable_names(self) -> List[str]: raise NotImplementedError @dbus_method_async() - async def list_names(self) -> List[str]: + async def list_names(self) -> list[str]: """List all services and connections currently of the bus. :return: List of all current names. @@ -134,7 +134,7 @@ async def start_service_by_name( raise NotImplementedError @dbus_property_async('as') - def features(self) -> List[str]: + def features(self) -> list[str]: """List of D-Bus daemon features. Features include: @@ -149,7 +149,7 @@ def features(self) -> List[str]: raise NotImplementedError @dbus_property_async('as') - def interfaces(self) -> List[str]: + def interfaces(self) -> list[str]: """Extra D-Bus daemon interfaces""" raise NotImplementedError @@ -164,7 +164,7 @@ def name_lost(self) -> str: raise NotImplementedError @dbus_signal_async('sss') - def name_owner_changed(self) -> Tuple[str, str, str]: + def name_owner_changed(self) -> tuple[str, str, str]: """Signal when some name on a bus changes owner. Is a tuple of: diff --git a/src/sdbus_block/dbus_daemon/__init__.py b/src/sdbus_block/dbus_daemon/__init__.py index 1b321cd..e8bdeba 100644 --- a/src/sdbus_block/dbus_daemon/__init__.py +++ b/src/sdbus_block/dbus_daemon/__init__.py @@ -19,7 +19,7 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA from __future__ import annotations -from typing import List, Optional +from typing import Optional from sdbus import DbusInterfaceCommon, SdBus, dbus_method, dbus_property @@ -84,7 +84,7 @@ def get_name_owner(self, service_name: str) -> str: raise NotImplementedError @dbus_method() - def list_activatable_names(self) -> List[str]: + def list_activatable_names(self) -> list[str]: """Lists all activatable services names. :return: List of all names. @@ -92,7 +92,7 @@ def list_activatable_names(self) -> List[str]: raise NotImplementedError @dbus_method() - def list_names(self) -> List[str]: + def list_names(self) -> list[str]: """List all services and connections currently of the bus. :return: List of all current names. @@ -125,7 +125,7 @@ def start_service_by_name( raise NotImplementedError @dbus_property('as') - def features(self) -> List[str]: + def features(self) -> list[str]: """List of D-Bus daemon features. Features include: @@ -140,6 +140,6 @@ def features(self) -> List[str]: raise NotImplementedError @dbus_property('as') - def interfaces(self) -> List[str]: + def interfaces(self) -> list[str]: """Extra D-Bus daemon interfaces""" raise NotImplementedError diff --git a/test/leak_tests.py b/test/leak_tests.py index 3326f43..59dcf0c 100644 --- a/test/leak_tests.py +++ b/test/leak_tests.py @@ -29,7 +29,7 @@ ) from os import environ from resource import RUSAGE_SELF, getrusage -from typing import Any, List, cast +from typing import Any, cast from unittest import SkipTest from sdbus.exceptions import DbusFailedError @@ -189,7 +189,7 @@ async def the_test() -> None: nonlocal i i += 1 - tasks: List[Task[None]] = [] + tasks: list[Task[None]] = [] loop = get_running_loop() for _ in range(num_of_tasks): tasks.append(loop.create_task(the_test())) diff --git a/test/test_read_write_dbus_types.py b/test/test_read_write_dbus_types.py index 3f7ccb7..cac57ba 100644 --- a/test/test_read_write_dbus_types.py +++ b/test/test_read_write_dbus_types.py @@ -19,7 +19,6 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA from __future__ import annotations -from typing import Dict, List from unittest import main from sdbus.sd_bus_internals import SdBus, SdBusMessage @@ -171,7 +170,7 @@ def test_array(self) -> None: def test_empty_array(self) -> None: message = create_message(self.bus) - test_array: List[str] = [] + test_array: list[str] = [] message.append_data("as", test_array) message.seal() @@ -234,7 +233,7 @@ def test_dict(self) -> None: def test_empty_dict(self) -> None: message = create_message(self.bus) - test_dict: Dict[str, str] = {} + test_dict: dict[str, str] = {} message.append_data("a{ss}", test_dict) message.seal() diff --git a/test/test_sdbus_async.py b/test/test_sdbus_async.py index a2d4dc7..f17b11a 100644 --- a/test/test_sdbus_async.py +++ b/test/test_sdbus_async.py @@ -54,8 +54,6 @@ ) if TYPE_CHECKING: - from typing import Tuple - from sdbus.dbus_proxy_async_interfaces import ( DBUS_PROPERTIES_CHANGED_TYPING, ) @@ -173,7 +171,7 @@ async def kwargs_function_annotated( return input.lower() @dbus_signal_async('ss') - def test_signal(self) -> Tuple[str, str]: + def test_signal(self) -> tuple[str, str]: """Test signal""" raise NotImplementedError @@ -209,13 +207,13 @@ def test_constant_property(self) -> str: @dbus_method_async( result_signature='(ss)' ) - async def test_struct_return(self) -> Tuple[str, str]: + async def test_struct_return(self) -> tuple[str, str]: return ('hello', 'world') @dbus_method_async( result_signature='(ss)' ) - async def test_struct_return_workaround(self) -> Tuple[Tuple[str, str]]: + async def test_struct_return_workaround(self) -> tuple[tuple[str, str]]: return (('hello', 'world'), ) @dbus_method_async() @@ -236,7 +234,7 @@ async def returns_none_method(self) -> None: ) async def takes_struct_method( self, - int_struct: Tuple[int, int, int, int], + int_struct: tuple[int, int, int, int], ) -> int: a, b, c, d = int_struct return a*b*c*d @@ -257,7 +255,7 @@ class DbusErrorUnmappedLater(DbusFailedError): TEST_SERVICE_NAME = 'org.example.test' -def initialize_object() -> Tuple[TestInterface, TestInterface]: +def initialize_object() -> tuple[TestInterface, TestInterface]: test_object = TestInterface() test_object.export_to_dbus('/') @@ -510,7 +508,7 @@ async def test_signal_catch_anywhere(self) -> None: with self.subTest('Catch anywhere over D-Bus object'): async def catch_anywhere_oneshot_dbus( - ) -> Tuple[str, Tuple[str, str]]: + ) -> tuple[str, tuple[str, str]]: async for x in test_object_connection.test_signal\ .catch_anywhere(): return x @@ -531,7 +529,7 @@ async def catch_anywhere_oneshot_dbus( with self.subTest('Catch anywhere over D-Bus class'): async def catch_anywhere_oneshot_from_class( - ) -> Tuple[str, Tuple[str, str]]: + ) -> tuple[str, tuple[str, str]]: async for x in TestInterface.test_signal.catch_anywhere( TEST_SERVICE_NAME, self.bus): return x @@ -552,7 +550,7 @@ async def catch_anywhere_oneshot_from_class( with self.subTest('Catch anywhere over local object'): async def catch_anywhere_oneshot_local( - ) -> Tuple[str, Tuple[str, str]]: + ) -> tuple[str, tuple[str, str]]: async for x in test_object.test_signal.catch_anywhere(): return x @@ -574,13 +572,13 @@ async def test_signal_multiple_readers(self) -> None: test_tuple = ('sgfsretg', 'asd') - async def reader_one() -> Tuple[str, str]: + async def reader_one() -> tuple[str, str]: async for x in test_object_connection.test_signal.catch(): return test_tuple raise RuntimeError - async def reader_two() -> Tuple[str, str]: + async def reader_two() -> tuple[str, str]: async for x in test_object_connection.test_signal.catch(): return test_tuple diff --git a/test/test_sdbus_async_introspection.py b/test/test_sdbus_async_introspection.py index 2ad8e46..fe17b7e 100644 --- a/test/test_sdbus_async_introspection.py +++ b/test/test_sdbus_async_introspection.py @@ -19,21 +19,16 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA from __future__ import annotations -from typing import TYPE_CHECKING - from sdbus.unittest import IsolatedDbusTestCase from sdbus import DbusInterfaceCommonAsync, dbus_method_async -if TYPE_CHECKING: - from typing import Tuple, Type - TEST_SERVICE_NAME = 'org.example.test' def initialize_object( - interface_class: Type[DbusInterfaceCommonAsync], -) -> Tuple[DbusInterfaceCommonAsync, DbusInterfaceCommonAsync]: + interface_class: type[DbusInterfaceCommonAsync], +) -> tuple[DbusInterfaceCommonAsync, DbusInterfaceCommonAsync]: test_object = interface_class() test_object.export_to_dbus('/') diff --git a/test/test_sdbus_block.py b/test/test_sdbus_block.py index 7618d81..e267f28 100644 --- a/test/test_sdbus_block.py +++ b/test/test_sdbus_block.py @@ -17,7 +17,6 @@ # 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 unittest import main diff --git a/test/test_typing.py b/test/test_typing.py index a84b872..e6e956c 100644 --- a/test/test_typing.py +++ b/test/test_typing.py @@ -19,8 +19,6 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA from __future__ import annotations -from typing import TYPE_CHECKING - from sdbus import ( DbusInterfaceCommon, DbusInterfaceCommonAsync, @@ -31,9 +29,6 @@ dbus_signal_async, ) -if TYPE_CHECKING: - from typing import List - class TestTypingBlocking( DbusInterfaceCommon, @@ -41,11 +36,11 @@ class TestTypingBlocking( ): @dbus_method(result_signature="as") - def get_str_list_method(self) -> List[str]: + def get_str_list_method(self) -> list[str]: raise NotImplementedError @dbus_property("as") - def str_list_property(self) -> List[str]: + def str_list_property(self) -> list[str]: raise NotImplementedError @@ -82,15 +77,15 @@ class TestTypingAsync( ): @dbus_method_async(result_signature="as") - async def get_str_list_method(self) -> List[str]: + async def get_str_list_method(self) -> list[str]: raise NotImplementedError @dbus_property_async("as") - def str_list_property(self) -> List[str]: + def str_list_property(self) -> list[str]: raise NotImplementedError @dbus_signal_async("as") - def str_list_signal(self) -> List[str]: + def str_list_signal(self) -> list[str]: raise NotImplementedError @@ -158,7 +153,7 @@ async def check_async_interface_signal_typing( async def check_async_element_class_access_typing() -> None: - test_list: List[str] = [] + test_list: list[str] = [] # TODO: Fix dbus async method typing # test_list.append( diff --git a/tools/run_py_linters.py b/tools/run_py_linters.py index 1312fde..091ae00 100755 --- a/tools/run_py_linters.py +++ b/tools/run_py_linters.py @@ -23,7 +23,6 @@ from os import environ from pathlib import Path from subprocess import SubprocessError, run -from typing import List source_root = Path(environ['MESON_SOURCE_ROOT']) build_dir = Path(environ['MESON_BUILD_ROOT']) @@ -51,7 +50,7 @@ def run_mypy() -> None: args=( 'mypy', '--strict', '--pretty', '--cache-dir', mypy_cache_dir, - '--python-version', '3.8', + '--python-version', '3.9', '--namespace-packages', '--explicit-package-bases', *all_python_modules, @@ -88,8 +87,8 @@ def linter_main() -> None: raise SystemExit(1) -def get_all_python_files() -> List[Path]: - python_files: List[Path] = [source_root / 'setup.py'] +def get_all_python_files() -> list[Path]: + python_files: list[Path] = [source_root / 'setup.py'] for python_module in all_python_modules: if python_module.is_dir(): diff --git a/wheel-build/run_inside_container.py b/wheel-build/run_inside_container.py index 8b7fe75..100a8fc 100755 --- a/wheel-build/run_inside_container.py +++ b/wheel-build/run_inside_container.py @@ -25,9 +25,8 @@ from pathlib import Path from shutil import copy from subprocess import PIPE, CalledProcessError, run -from typing import List -yum_packages: List[str] = [ +yum_packages: list[str] = [ 'gettext-autopoint', 'gperf', ] @@ -53,14 +52,14 @@ ROOT_DIR = Path('/root') NPROC = '4' -PYTHON_VERSIONS = ['cp39-cp39', 'cp38-cp38', 'cp37-cp37m'] +PYTHON_VERSIONS = ['cp39-cp39'] -BASIC_C_FLAGS: List[str] = [ +BASIC_C_FLAGS: list[str] = [ '-O2', '-fno-plt', '-D_FORTIFY_SOURCE=2', '-fstack-clash-protection', ] -SYSTEMD_OPTIONS: List[str] = [ +SYSTEMD_OPTIONS: list[str] = [ "static-libsystemd=pic", "tests=false", "coredump=false", From f6052fb32e78ef5958e1e6ae01ef893967b9bc42 Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sun, 2 Feb 2025 22:13:12 +0000 Subject: [PATCH 137/188] Remove unnecessary generator expressions brackets When generator expression is used inside another brackets it does not need extra brackets. --- src/sdbus/dbus_proxy_async_interface_base.py | 2 +- src/sdbus/dbus_proxy_sync_interface_base.py | 2 +- src/sdbus/interface_generator.py | 14 +++++++------- test/test_low_level_api.py | 6 ++---- 4 files changed, 11 insertions(+), 13 deletions(-) diff --git a/src/sdbus/dbus_proxy_async_interface_base.py b/src/sdbus/dbus_proxy_async_interface_base.py index 6beb8d1..ffeba91 100644 --- a/src/sdbus/dbus_proxy_async_interface_base.py +++ b/src/sdbus/dbus_proxy_async_interface_base.py @@ -256,7 +256,7 @@ def __new__(cls, name: str, ) all_mro_bases: set[type[Any]] = set( - chain.from_iterable((c.__mro__ for c in bases)) + chain.from_iterable(c.__mro__ for c in bases) ) reserved_dbus_map = cls._map_mro_dbus_elements( name, all_mro_bases, diff --git a/src/sdbus/dbus_proxy_sync_interface_base.py b/src/sdbus/dbus_proxy_sync_interface_base.py index 0290bb4..dfd3bda 100644 --- a/src/sdbus/dbus_proxy_sync_interface_base.py +++ b/src/sdbus/dbus_proxy_sync_interface_base.py @@ -132,7 +132,7 @@ def __new__(cls, name: str, ) all_mro_bases: set[type[Any]] = set( - chain.from_iterable((c.__mro__ for c in bases)) + chain.from_iterable(c.__mro__ for c in bases) ) reserved_attr_names = cls._collect_dbus_to_python_attr_names( name, all_mro_bases, diff --git a/src/sdbus/interface_generator.py b/src/sdbus/interface_generator.py index 1272d81..250c8d0 100644 --- a/src/sdbus/interface_generator.py +++ b/src/sdbus/interface_generator.py @@ -250,7 +250,7 @@ def result_typing(cls, result_args: list[str]) -> str: return cls.typing_complete(result_args[0]) else: return cls.typing_into_tuple( - (cls.typing_complete(x) for x in result_args) + cls.typing_complete(x) for x in result_args ) @classmethod @@ -380,14 +380,14 @@ def _parse_arg(self, arg: Element) -> None: @property def dbus_input_signature(self) -> str: return ''.join( - (x.dbus_type for x in self.input_args) + x.dbus_type for x in self.input_args ) @property def dbus_result_signature(self) -> str: return ''.join( - (x.dbus_type if not x.is_input else '' - for x in self.result_args) + x.dbus_type if not x.is_input else '' + for x in self.result_args ) @property @@ -411,7 +411,7 @@ def result_typing(self) -> str: @property def is_results_args_valid_names(self) -> bool: - return all((r.name is not None for r in self.result_args)) + return all(r.name is not None for r in self.result_args) @property def result_args_names_repr(self) -> str: @@ -512,7 +512,7 @@ def _parse_arg(self, arg: Element) -> None: @property def dbus_signature(self) -> str: - return ''.join((x.dbus_type for x in self.args)) + return ''.join(x.dbus_type for x in self.args) @property def typing(self) -> str: @@ -521,7 +521,7 @@ def typing(self) -> str: @property def is_args_valid_names(self) -> bool: - return all((a.name is not None for a in self.args)) + return all(a.name is not None for a in self.args) @property def args_names_repr(self) -> str: diff --git a/test/test_low_level_api.py b/test/test_low_level_api.py index e622ed5..1030bb3 100644 --- a/test/test_low_level_api.py +++ b/test/test_low_level_api.py @@ -86,10 +86,8 @@ def test_validation_funcs(self) -> None: ) except NotImplementedError: raise SkipTest( - ( - "Validation funcs not implemented. " - "Probably too old libsystemd. (< 246)" - ) + "Validation funcs not implemented. " + "Probably too old libsystemd. (< 246)" ) def test_bus_method_call_timeout(self) -> None: From 75c64cb1b517d9a3837110460068c0a8020c9255 Mon Sep 17 00:00:00 2001 From: Arkadiusz Bokowy Date: Fri, 21 Feb 2025 16:47:32 +0100 Subject: [PATCH 138/188] docs: Fix ObjectManager example missing call argument --- docs/asyncio_api.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/asyncio_api.rst b/docs/asyncio_api.rst index 0b94711..3411f2f 100644 --- a/docs/asyncio_api.rst +++ b/docs/asyncio_api.rst @@ -185,7 +185,7 @@ Classes my_object_manager.export_to_dbus('/object/manager') managed_object = DbusInterfaceCommonAsync() - my_object_manager.export_with_manager('/object/manager/example') + my_object_manager.export_with_manager('/object/manager/example', managed_object) .. py:method:: get_managed_objects() :async: From c6b3705586993b0c9cb3ccf781d4967100330828 Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sat, 8 Mar 2025 16:23:37 +0000 Subject: [PATCH 139/188] Fix code generator adding result_args_names arg to blocking methods This argument is only supported by async methods because it is used when serving objects. Reported by @christophehenry. --- src/sdbus/interface_generator.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/sdbus/interface_generator.py b/src/sdbus/interface_generator.py index 250c8d0..41b9c78 100644 --- a/src/sdbus/interface_generator.py +++ b/src/sdbus/interface_generator.py @@ -588,9 +588,6 @@ def has_members(self) -> bool: {% if method.dbus_result_signature %} result_signature="{{ method.dbus_result_signature }}", {% endif %} -{% if method.is_results_args_valid_names %} -result_args_names={{method.result_args_names_repr}}, -{% endif %} {% if method.flags_str %} flags={{ method.flags_str }}, {% endif %} @@ -674,6 +671,9 @@ class {{ interface.python_name }}( {% filter indent(first=True) %} {% include 'generic_method_flags' %} {% endfilter %} +{% if method.is_results_args_valid_names %} + result_args_names={{method.result_args_names_repr}}, +{% endif %} ) async def {{ method.python_name }}( self, From ce18c8232fd2e2e8cd4eddfa5e224caa988f8db8 Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sat, 8 Mar 2025 17:24:35 +0000 Subject: [PATCH 140/188] Make code generator handle members where name cannot be converted back Certain member names like `GetURL` will be converted to Python name `get_url` but when converted back it will become `GetUrl`. This will cause methods and properties with those names be unaccessible. However, method, properties and signals allow setting the D-Bus name instead of relying on auto conversion. Make code generator use those arguments where auto conversion does not produce equal names between D-Bus and Python. Reported by @nicomuns. --- src/sdbus/dbus_common_elements.py | 11 ++++------- src/sdbus/dbus_common_funcs.py | 8 ++++++-- src/sdbus/interface_generator.py | 15 +++++++++++++++ 3 files changed, 25 insertions(+), 9 deletions(-) diff --git a/src/sdbus/dbus_common_elements.py b/src/sdbus/dbus_common_elements.py index 13d2a81..ac49be1 100644 --- a/src/sdbus/dbus_common_elements.py +++ b/src/sdbus/dbus_common_elements.py @@ -24,8 +24,8 @@ from .dbus_common_funcs import ( _is_property_flags_correct, - _method_name_converter, get_default_bus, + snake_case_to_camel_case, ) from .sd_bus_internals import is_interface_name_valid, is_member_name_valid @@ -122,8 +122,7 @@ def __init__( " it in to a tuple ('string', ) ?") if method_name is None: - method_name = ''.join( - _method_name_converter(original_method.__name__)) + method_name = snake_case_to_camel_case(original_method.__name__) try: assert is_member_name_valid(method_name), ( @@ -230,8 +229,7 @@ def __init__(self, flags: int, original_method: FunctionType): if property_name is None: - property_name = ''.join( - _method_name_converter(original_method.__name__)) + property_name = snake_case_to_camel_case(original_method.__name__) try: assert is_member_name_valid(property_name), ( @@ -262,8 +260,7 @@ def __init__(self, flags: int, original_method: FunctionType): if signal_name is None: - signal_name = ''.join( - _method_name_converter(original_method.__name__)) + signal_name = snake_case_to_camel_case(original_method.__name__) try: assert is_member_name_valid(signal_name), ( diff --git a/src/sdbus/dbus_common_funcs.py b/src/sdbus/dbus_common_funcs.py index f7122b2..77dfbb3 100644 --- a/src/sdbus/dbus_common_funcs.py +++ b/src/sdbus/dbus_common_funcs.py @@ -135,8 +135,8 @@ def request_default_bus_name( return _DeprecationAwaitable() -def _method_name_converter(python_name: str) -> Iterator[str]: - char_iter = iter(python_name) +def _snake_case_to_camel_case_gen(snake: str) -> Iterator[str]: + char_iter = iter(snake) # Name starting with upper case letter try: first_char = next(char_iter) @@ -158,6 +158,10 @@ def _method_name_converter(python_name: str) -> Iterator[str]: upper_next_one = True +def snake_case_to_camel_case(snake: str) -> str: + return "".join(_snake_case_to_camel_case_gen(snake)) + + def _check_sync_in_async_env() -> bool: try: get_running_loop() diff --git a/src/sdbus/interface_generator.py b/src/sdbus/interface_generator.py index 41b9c78..de26c7c 100644 --- a/src/sdbus/interface_generator.py +++ b/src/sdbus/interface_generator.py @@ -24,6 +24,8 @@ from xml.etree.ElementTree import fromstring as etree_from_str from xml.etree.ElementTree import parse as etree_from_file +from .dbus_common_funcs import snake_case_to_camel_case + if TYPE_CHECKING: from collections.abc import Iterable, Iterator from typing import Literal, Optional, Union @@ -317,6 +319,10 @@ def iter_sub_elements(self, element: Element) -> None: raise ValueError( 'Uknown member annotation tag: ', tag) + @property + def wants_rename(self) -> bool: + return self.method_name != snake_case_to_camel_case(self.python_name) + class DbusArgsIntrospection: def __init__(self, element: Element): @@ -591,6 +597,9 @@ def has_members(self) -> bool: {% if method.flags_str %} flags={{ method.flags_str }}, {% endif %} +{% if method.wants_rename %} +method_name="{{method.method_name}}", +{% endif %} """ ), "generic_property_flags": ( @@ -601,6 +610,9 @@ def has_members(self) -> bool: {% if a_property.flags_str %} flags={{ a_property.flags_str }}, {% endif %} +{% if a_property.wants_rename %} +property_name="{{a_property.method_name}}", +{% endif %} """ ), "generic_header": """\ @@ -709,6 +721,9 @@ def {{ a_property.python_name }}(self) -> {{ a_property.typing }}: {% if signal.flags_str %} flags={{ signal.flags_str }}, {% endif %} +{% if signal.wants_rename %} + signal_name=signal.method_name, +{% endif %} ) def {{ signal.python_name }}(self) -> {{ signal.typing }}: raise NotImplementedError From 44db8f424318ddd80f877e355c93296f0e8dcbe4 Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sun, 23 Mar 2025 18:47:52 +0000 Subject: [PATCH 141/188] Make all parsing helpers accept the blocking interfaces Some parsing functions like `parse_get_managed_objects` can be used on data that can be obtained from blocking interfaces. No reason to make parsing functions exclusive to async interfaces. Add extra unit tests to verify the behaviour. --- src/sdbus/utils/parse.py | 59 +++++++++------ test/test_sdbus_utils.py | 159 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 197 insertions(+), 21 deletions(-) diff --git a/src/sdbus/utils/parse.py b/src/sdbus/utils/parse.py index de09386..1a17c7d 100644 --- a/src/sdbus/utils/parse.py +++ b/src/sdbus/utils/parse.py @@ -27,6 +27,7 @@ DBUS_INTERFACE_NAME_TO_CLASS, DbusInterfaceBaseAsync, ) +from ..dbus_proxy_sync_interface_base import DbusInterfaceBase if TYPE_CHECKING: from collections.abc import Iterable @@ -34,23 +35,42 @@ from ..dbus_proxy_async_interfaces import DBUS_PROPERTIES_CHANGED_TYPING - InterfacesInputElements = Union[ - DbusInterfaceBaseAsync, - type[DbusInterfaceBaseAsync], - ] + InterfacesBaseClasses = Union[DbusInterfaceBaseAsync, DbusInterfaceBase] + InterfacesBaseTypes = type[InterfacesBaseClasses] + InterfacesInputElements = Union[InterfacesBaseClasses, InterfacesBaseTypes] InterfacesInput = Union[ InterfacesInputElements, Iterable[InterfacesInputElements], ] - InterfacesToClassMap = dict[frozenset[str], type[DbusInterfaceBaseAsync]] + InterfacesToClassMap = dict[ + frozenset[str], + type[Union[DbusInterfaceBaseAsync, DbusInterfaceBase]], + ] OnUnknownMember = Literal['error', 'ignore', 'reuse'] OnUnknownInterface = Literal['error', 'none'] ParseGetManaged = dict[ str, - tuple[Optional[type[DbusInterfaceBaseAsync]], dict[str, Any]], + tuple[ + Optional[InterfacesBaseTypes], + dict[str, Any], + ], ] +def _interfaces_input_to_types( + interfaces: InterfacesInput, +) -> tuple[InterfacesBaseTypes, ...]: + if isinstance( + interfaces, + (DbusInterfaceBaseAsync, DbusInterfaceBase, type) + ): + return ( + interfaces if isinstance(interfaces, type) else type(interfaces), + ) + else: + return tuple(i if isinstance(i, type) else type(i) for i in interfaces) + + def parse_properties_changed( interface: InterfacesInputElements, properties_changed_data: DBUS_PROPERTIES_CHANGED_TYPING, @@ -81,18 +101,12 @@ def parse_properties_changed( def _create_interfaces_map( - interfaces: InterfacesInput, + interfaces: tuple[InterfacesBaseTypes, ...], ) -> InterfacesToClassMap: - if isinstance(interfaces, - (DbusInterfaceBaseAsync, type)): - interfaces_iter = iter((interfaces, )) - else: - interfaces_iter = iter(interfaces) - interfaces_to_class_map: InterfacesToClassMap = {} - for interface in interfaces_iter: + for interface in interfaces: interface_names_set = frozenset( interface_name for interface_name, _ in interface._dbus_iter_interfaces_meta() @@ -110,7 +124,7 @@ def _get_class_from_interfaces( interfaces_to_class_map: InterfacesToClassMap, interface_names_iter: Iterable[str], raise_key_error: bool, -) -> Optional[type[DbusInterfaceBaseAsync]]: +) -> Optional[InterfacesBaseTypes]: class_set = frozenset(interface_names_iter) - SKIP_INTERFACES try: return interfaces_to_class_map[class_set] @@ -122,7 +136,7 @@ def _get_class_from_interfaces( def _get_member_map_from_class( - python_class: Optional[type[DbusInterfaceBaseAsync]], + python_class: Optional[InterfacesBaseTypes], ) -> dict[str, dict[str, str]]: if python_class is None: return {} @@ -160,9 +174,10 @@ def parse_interfaces_added( interfaces_added_data: tuple[str, dict[str, dict[str, Any]]], on_unknown_interface: OnUnknownInterface = 'error', on_unknown_member: OnUnknownMember = 'error', -) -> tuple[str, Optional[type[DbusInterfaceBaseAsync]], dict[str, Any]]: +) -> tuple[str, Optional[InterfacesBaseTypes], dict[str, Any]]: - interfaces_to_class_map = _create_interfaces_map(interfaces) + interfaces_types = _interfaces_input_to_types(interfaces) + interfaces_to_class_map = _create_interfaces_map(interfaces_types) path, properties_data = interfaces_added_data @@ -202,9 +217,10 @@ def parse_interfaces_removed( interfaces: InterfacesInput, interfaces_removed_data: tuple[str, list[str]], on_unknown_interface: OnUnknownInterface = 'error', -) -> tuple[str, Optional[type[DbusInterfaceBaseAsync]]]: +) -> tuple[str, Optional[InterfacesBaseTypes]]: - interfaces_to_class_map = _create_interfaces_map(interfaces) + interfaces_types = _interfaces_input_to_types(interfaces) + interfaces_to_class_map = _create_interfaces_map(interfaces_types) path, interfaces_removed = interfaces_removed_data @@ -226,7 +242,8 @@ def parse_get_managed_objects( on_unknown_member: OnUnknownMember = 'error', ) -> ParseGetManaged: - interfaces_to_class_map = _create_interfaces_map(interfaces) + interfaces_types = _interfaces_input_to_types(interfaces) + interfaces_to_class_map = _create_interfaces_map(interfaces_types) managed_objects_map: ParseGetManaged = {} diff --git a/test/test_sdbus_utils.py b/test/test_sdbus_utils.py index 18b3050..c9fa774 100644 --- a/test/test_sdbus_utils.py +++ b/test/test_sdbus_utils.py @@ -19,18 +19,177 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA from __future__ import annotations +from unittest import TestCase + from sdbus.unittest import IsolatedDbusTestCase from sdbus.utils.inspect import inspect_dbus_path +from sdbus.utils.parse import parse_get_managed_objects from sdbus import ( DbusInterfaceCommon, DbusInterfaceCommonAsync, + dbus_property, + dbus_property_async, sd_bus_open_user, ) TEST_PATH = "/test" +class FooAsync(DbusInterfaceCommonAsync, interface_name="org.foo"): + @dbus_property_async("x") + def foo(self) -> int: + return 1 + + +class BarAsync(DbusInterfaceCommonAsync, interface_name="org.bar"): + @dbus_property_async("x") + def bar(self) -> int: + return 2 + + +class FooBarAsync(FooAsync, BarAsync): + ... + + +class Foo(DbusInterfaceCommon, interface_name="org.foo"): + @dbus_property("x") + def foo(self) -> int: + return 1 + + +class Bar(DbusInterfaceCommon, interface_name="org.bar"): + @dbus_property("x") + def bar(self) -> int: + return 2 + + +class FooBar(Foo, Bar): + ... + + +MANAGED_OBJECTS_COMBINED = { + "/test": { + "org.foo": {"Foo": ("x", 1)}, + "org.bar": {"Bar": ("x", 2)}, + } +} + +MANAGED_OBJECTS_SPLIT = { + "/foo": { + "org.foo": {"Foo": ("x", 1)}, + }, + "/bar": { + "org.bar": {"Bar": ("x", 2)}, + }, +} + +MANAGED_OBJECTS_BOTH = {**MANAGED_OBJECTS_COMBINED, **MANAGED_OBJECTS_SPLIT} + + +class TestSdbusUtilsParse(TestCase): + def test_parse_get_managed_objects_async_combined(self) -> None: + parsed_managed = parse_get_managed_objects( + FooBarAsync, + MANAGED_OBJECTS_COMBINED, + ) + + self.assertEqual(1, len(parsed_managed)) + + class_type, properties_data = parsed_managed["/test"] + self.assertEqual(FooBarAsync, class_type) + self.assertEqual(properties_data["foo"], 1) + self.assertEqual(properties_data["bar"], 2) + + def test_parse_get_managed_objects_block_combined(self) -> None: + parsed_managed = parse_get_managed_objects( + FooBar, + MANAGED_OBJECTS_COMBINED, + ) + + self.assertEqual(1, len(parsed_managed)) + + class_type, properties_data = parsed_managed["/test"] + self.assertEqual(FooBar, class_type) + self.assertEqual(properties_data["foo"], 1) + self.assertEqual(properties_data["bar"], 2) + + def test_parse_get_managed_objects_async_split(self) -> None: + parsed_managed = parse_get_managed_objects( + [FooAsync, BarAsync], + MANAGED_OBJECTS_SPLIT, + ) + + self.assertEqual(2, len(parsed_managed)) + + class_type, properties_data = parsed_managed["/foo"] + self.assertEqual(FooAsync, class_type) + self.assertEqual(properties_data["foo"], 1) + + class_type, properties_data = parsed_managed["/bar"] + self.assertEqual(BarAsync, class_type) + self.assertEqual(properties_data["bar"], 2) + + def test_parse_get_managed_objects_block_split(self) -> None: + parsed_managed = parse_get_managed_objects( + [Foo, Bar], + MANAGED_OBJECTS_SPLIT, + ) + + self.assertEqual(2, len(parsed_managed)) + + class_type, properties_data = parsed_managed["/foo"] + self.assertEqual(Foo, class_type) + self.assertEqual(properties_data["foo"], 1) + + class_type, properties_data = parsed_managed["/bar"] + self.assertEqual(Bar, class_type) + self.assertEqual(properties_data["bar"], 2) + + def test_parse_get_managed_objects_unknown_interface_error(self) -> None: + with self.assertRaisesRegex(KeyError, "org.foo"): + parse_get_managed_objects( + Bar, + MANAGED_OBJECTS_SPLIT, + ) + + def test_parse_get_managed_objects_unknown_interface_none_reuse( + self, + ) -> None: + parsed_managed = parse_get_managed_objects( + {BarAsync}, + MANAGED_OBJECTS_SPLIT, + on_unknown_interface="none", + on_unknown_member="reuse", + ) + + class_type, properties_data = parsed_managed["/foo"] + self.assertIsNone(class_type) + self.assertEqual(properties_data["Foo"], 1) + + class_type, properties_data = parsed_managed["/bar"] + self.assertEqual(BarAsync, class_type) + self.assertEqual(properties_data["bar"], 2) + + def test_parse_get_managed_objects_unknown_member_skip(self) -> None: + parsed_managed = parse_get_managed_objects( + [Foo], + MANAGED_OBJECTS_SPLIT, + on_unknown_interface="none", + on_unknown_member="ignore", + ) + + self.assertEqual(2, len(parsed_managed)) + + class_type, properties_data = parsed_managed["/foo"] + self.assertEqual(Foo, class_type) + self.assertEqual(properties_data["foo"], 1) + + class_type, properties_data = parsed_managed["/bar"] + self.assertIsNone(class_type) + self.assertEqual(0, len(properties_data)) + + class TestSdbusUtilsInspect(IsolatedDbusTestCase): def test_inspect_dbus_path_block(self) -> None: proxy = DbusInterfaceCommon("example.org", TEST_PATH) From 12c41ca86c8f22b49c47393bc92551aec4363ea2 Mon Sep 17 00:00:00 2001 From: igo95862 Date: Wed, 26 Mar 2025 16:36:11 +0000 Subject: [PATCH 142/188] Document sdbus.utils.parse with autodoc This will make it easier to document the function overloads in the future. Also update the interfaces input description given that it now can accept the blocking interfaces. --- docs/conf.py | 1 - docs/utils.rst | 79 +------------------------------ src/sdbus/utils/parse.py | 100 +++++++++++++++++++++++++++++++++++++-- 3 files changed, 99 insertions(+), 81 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index f61f4f9..b165939 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -28,7 +28,6 @@ html_theme = "sphinx_rtd_theme" autoclass_content = 'both' -autodoc_typehints = 'description' autodoc_member_order = 'bysource' path.insert(0, abspath('../src')) diff --git a/docs/utils.rst b/docs/utils.rst index b1b3b4b..83a9b1a 100644 --- a/docs/utils.rst +++ b/docs/utils.rst @@ -7,83 +7,8 @@ Parsing utilities Parse unweildy D-Bus structures in to Python native objects and names. Available under ``sdbus.utils.parse`` subpackage. -.. py:currentmodule:: sdbus.utils.parse - -.. py:function:: parse_properties_changed(interface, properties_changed_data, on_unknown_member='error') - - Parse data from :py:meth:`properties_changed ` signal. - - Member names will be translated to python defined names. - Invalidated properties will have a value of None. - - :param DbusInterfaceBaseAsync interface: Takes either D-Bus interface or interface class. - :param Tuple properties_changed_data: Tuple caught from signal. - :param str on_unknown_member: If an unknown D-Bus property was encountered - either raise an ``"error"`` (default), ``"ignore"`` the property - or ``"reuse"`` the D-Bus name for the member. - :rtype: dict[str, Any] - :returns: Dictionary of changed properties with keys translated to python - names. Invalidated properties will have value of None. - -.. py:function:: parse_interfaces_added(interfaces, interfaces_added_data, on_unknown_interface='error', on_unknown_member='error') - - Parse data from :py:meth:`interfaces_added ` signal. - - Takes an iterable of D-Bus interface classes (or a single class) and the signal data. - Returns the path of new object, the class of the added object (if it matched one of passed interface classes) - and the dictionary of python named properties and their values. - - :param Iterable[DbusInterfaceBaseAsync] interfaces: Possible interfaces that were added. - Can accept classes with multiple interfaces defined. - :param Tuple interfaces_added_data: Tuple caught from signal. - :param str on_unknown_interface: If an unknown D-Bus interface was encountered - either raise an ``"error"`` (default) or return ``"none"`` instead - of interface class. - :param str on_unknown_member: If an unknown D-Bus property was encountered - either raise an ``"error"`` (default), ``"ignore"`` the property - or ``"reuse"`` the D-Bus name for the member. - :rtype: tuple[str, Optional[type[DbusInterfaceBaseAsync]], dict[str, Any]] - :returns: Path of new added object, object's class (or ``None``) and dictionary - of python translated members and their values. - -.. py:function:: parse_interfaces_removed(interfaces, interfaces_removed_data, on_unknown_interface='error') - - Parse data from :py:meth:`interfaces_added ` signal. - - Takes an iterable of D-Bus interface classes (or a single class) and the signal data. - Returns the path of removed object and the class of the added object. - (if it matched one of passed interface classes) - - :param Iterable[DbusInterfaceBaseAsync] interfaces: Possible interfaces that were removed. - Can accept classes with multiple interfaces defined. - :param Tuple interfaces_added_data: Tuple caught from signal. - :param str on_unknown_member: If an unknown D-Bus interface was encountered - either raise an ``"error"`` (default) or return ``"none"`` instead - of interface class. - :rtype: tuple[str, Optional[type[DbusInterfaceBaseAsync]]] - :returns: Path of removed object and object's class (or ``None``). - -.. py:function:: parse_get_managed_objects(interfaces, managed_objects_data, on_unknown_interface='error', on_unknown_member='error') - - Parse data from :py:meth:`get_managed_objects ` call. - - Takes an iterable of D-Bus interface classes (or a single class) and the method returned data. - Returns a dictionary where keys a paths of the managed objects and value is a tuple of class of the object - and dictionary of its python named properties and their values. - - :param Iterable[DbusInterfaceBaseAsync] interfaces: Possible interfaces of the managed objects. - Can accept classes with multiple interfaces defined. - :param Dict managed_objects_data: Data returned by ``get_managed_objects`` call. - :param str on_unknown_interface: If an unknown D-Bus interface was encountered - either raise an ``"error"`` (default) or return ``"none"`` instead - of interface class. - :param str on_unknown_member: If an unknown D-Bus property was encountered - either raise an ``"error"`` (default), ``"ignore"`` the property - or ``"reuse"`` the D-Bus name for the member. - :rtype: dict[str, tuple[Optional[type[DbusInterfaceBaseAsync], dict[str, Any]]]] - :returns: Dictionary where keys are paths and values are tuples of managed objects classes and their properties data. - - *New in version 0.12.0.* +.. automodule:: sdbus.utils.parse + :members: Inspect utilities +++++++++++++++++ diff --git a/src/sdbus/utils/parse.py b/src/sdbus/utils/parse.py index 1a17c7d..6138311 100644 --- a/src/sdbus/utils/parse.py +++ b/src/sdbus/utils/parse.py @@ -76,6 +76,28 @@ def parse_properties_changed( properties_changed_data: DBUS_PROPERTIES_CHANGED_TYPING, on_unknown_member: OnUnknownMember = 'error', ) -> dict[str, Any]: + """Parse data from :py:meth:`properties_changed \ + ` signal. + + Parses changed properties from a single D-Bus object. The object's + interface class must be known in advance and passed as a first + argument. + + Member names will be translated to python defined names. + Invalidated properties will have a value of None. + + :param interface: + Takes either D-Bus interface class or its object. + :param properties_changed_data: + Tuple caught from signal. + :param on_unknown_member: + If an unknown D-Bus property was encountered either raise + an ``"error"`` (default), ``"ignore"`` the property + or ``"reuse"`` the D-Bus name for the member. + :returns: + Dictionary of changed properties with keys translated to python + names. Invalidated properties will have value of None. + """ interface_name, changed_properties, invalidated_properties = ( properties_changed_data ) @@ -175,7 +197,33 @@ def parse_interfaces_added( on_unknown_interface: OnUnknownInterface = 'error', on_unknown_member: OnUnknownMember = 'error', ) -> tuple[str, Optional[InterfacesBaseTypes], dict[str, Any]]: - + """Parse data from :py:meth:`interfaces_added \ + ` signal. + + Takes the possible interface classes and the signal data. + Returns the path of new object, the class of the + added object (if it matched one of passed interface classes) + and the dictionary of python named properties and their values. + + The passed interfaces can be async or blocking, the class + or an instantiated object, a single item or an iterable of interfaces. + + :param interfaces: + Possible interfaces that were added. + :param interfaces_added_data: + Tuple caught from signal. + :param on_unknown_interface: + If an unknown D-Bus interface was encountered either raise + an ``"error"`` (default) or return ``"none"`` instead of + interface class. + :param on_unknown_member: + If an unknown D-Bus property was encountered either raise + an ``"error"`` (default), ``"ignore"`` the property + or ``"reuse"`` the D-Bus name for the member. + :returns: + Path of new added object, object's class (or ``None``) and dictionary + of python translated members and their values. + """ interfaces_types = _interfaces_input_to_types(interfaces) interfaces_to_class_map = _create_interfaces_map(interfaces_types) @@ -218,7 +266,26 @@ def parse_interfaces_removed( interfaces_removed_data: tuple[str, list[str]], on_unknown_interface: OnUnknownInterface = 'error', ) -> tuple[str, Optional[InterfacesBaseTypes]]: - + """Parse data from :py:meth:`interfaces_added \ + ` signal. + + Takes the possible interface classes and the signal data. + Returns the path and the matched class of removed object. + (if it matched one of passed interface classes) + + The passed interfaces can be async or blocking, the class + or an instantiated object, a single item or an iterable of interfaces. + + :param interfaces: + Possible interfaces that were removed. + :param interfaces_added_data: + Tuple caught from signal. + :param on_unknown_member: + If an unknown D-Bus interface was encountered either raise an + ``"error"`` (default) or return ``"none"`` instead of interface class. + :returns: + Path of removed object and object's class (or ``None``). + """ interfaces_types = _interfaces_input_to_types(interfaces) interfaces_to_class_map = _create_interfaces_map(interfaces_types) @@ -241,7 +308,34 @@ def parse_get_managed_objects( on_unknown_interface: OnUnknownInterface = 'error', on_unknown_member: OnUnknownMember = 'error', ) -> ParseGetManaged: - + """Parse data from :py:meth:`get_managed_objects \ + ` call. + + Takes the possible interface classes and the method's returned data. + Returns a dictionary where keys a paths of the managed objects and + value is a tuple of class of the object and dictionary of its python + named properties and their values. + + The passed interfaces can be async or blocking, the class + or an instantiated object, a single item or an iterable of interfaces. + + :param interfaces: + Possible interfaces of the managed objects. + :param managed_objects_data: + Data returned by ``get_managed_objects`` call. + :param on_unknown_interface: + If an unknown D-Bus interface was encountered either raise an + ``"error"`` (default) or return ``"none"`` instead of interface class. + :param on_unknown_member: + If an unknown D-Bus property was encountered either raise + an ``"error"`` (default), ``"ignore"`` the property + or ``"reuse"`` the D-Bus name for the member. + :returns: + Dictionary where keys are paths and values are tuples of managed + objects classes and their properties data. + + *New in version 0.12.0.* + """ interfaces_types = _interfaces_input_to_types(interfaces) interfaces_to_class_map = _create_interfaces_map(interfaces_types) From d7973e4f44be7150035c59c33f130b8a69283db4 Mon Sep 17 00:00:00 2001 From: igo95862 Date: Wed, 26 Mar 2025 16:48:32 +0000 Subject: [PATCH 143/188] Document sdbus.utils.inspect using autodoc Will make it easier to add typing overloads in the future. --- docs/utils.rst | 28 ++-------------------------- src/sdbus/utils/inspect.py | 24 ++++++++++++++++++++++++ 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/docs/utils.rst b/docs/utils.rst index 83a9b1a..f04ccde 100644 --- a/docs/utils.rst +++ b/docs/utils.rst @@ -17,29 +17,5 @@ Inspect D-Bus objects and retrieve their D-Bus related attributes such as D-Bus object paths and etc... Available under ``sdbus.utils.inspect`` subpackage. -.. py:currentmodule:: sdbus.utils.inspect - -.. py:function:: inspect_dbus_path(obj, bus=None) - - Returns the D-Bus path of an object. - - If called on a D-Bus proxy returns path of the proxied object. - - If called on a local D-Bus object returns the exported D-Bus path. - If object is not exported raises ``LookupError``. - - If called on an object that is unrelated to D-Bus raises ``TypeError``. - - The object's path is inspected in the context of the given bus and if the - object is attached to a different bus the ``LookupError`` will be raised. - If the bus argument is not given or is ``None`` the default bus will be - checked against. - - :param object obj: Object to inspect. - :param SdBus bus: - Bus to inspect against. - If not given or ``None`` the default bus will be used. - :rtype: str - :returns: D-Bus path of the object. - - *New in version 0.13.0.* +.. automodule:: sdbus.utils.inspect + :members: diff --git a/src/sdbus/utils/inspect.py b/src/sdbus/utils/inspect.py index a73e544..3aecdf8 100644 --- a/src/sdbus/utils/inspect.py +++ b/src/sdbus/utils/inspect.py @@ -71,6 +71,30 @@ def inspect_dbus_path( obj: Union[DbusInterfaceBase, DbusInterfaceBaseAsync], bus: Optional[SdBus] = None, ) -> str: + """Return the D-Bus path of an object. + + If called on a D-Bus proxy returns path of the proxied object. + + If called on a local D-Bus object returns the exported D-Bus path. + If object is not exported raises ``LookupError``. + + If called on an object that is unrelated to D-Bus raises ``TypeError``. + + The object's path is inspected in the context of the given bus and if the + object is attached to a different bus the ``LookupError`` will be raised. + If the bus argument is not given or is ``None`` the default bus will be + checked against. + + :param obj: + Object to inspect. + :param bus: + Bus to inspect against. + If not given or is ``None`` the default bus will be used. + :returns: + D-Bus path of the object. + + *New in version 0.13.0.* + """ if bus is None: bus = get_default_bus() From 01eb5d2ff4541c4825609b3a6d47d088ae754ea5 Mon Sep 17 00:00:00 2001 From: igo95862 Date: Wed, 26 Mar 2025 18:38:53 +0000 Subject: [PATCH 144/188] Fix exceptions mapped by map_exception_to_dbus_error not translating from Python to D-Bus This fixes the Python built-in exceptions not being translated when raised by sdbus server. Also the mapped exceptions will have their first argument added to the error message as string. --- src/sdbus/dbus_proxy_async_method.py | 17 +++++++---------- src/sdbus/sd_bus_internals.py | 4 ++-- src/sdbus/sd_bus_internals_funcs.c | 4 ++-- test/test_sdbus_async.py | 13 +++++++++++++ 4 files changed, 24 insertions(+), 14 deletions(-) diff --git a/src/sdbus/dbus_proxy_async_method.py b/src/sdbus/dbus_proxy_async_method.py index b05ea1d..49e304b 100644 --- a/src/sdbus/dbus_proxy_async_method.py +++ b/src/sdbus/dbus_proxy_async_method.py @@ -33,7 +33,7 @@ DbusRemoteObjectMeta, ) from .dbus_exceptions import DbusFailedError -from .sd_bus_internals import DbusNoReplyFlag +from .sd_bus_internals import EXCEPTION_TO_DBUS_ERROR, DbusNoReplyFlag if TYPE_CHECKING: from collections.abc import Callable, Sequence @@ -193,23 +193,20 @@ async def _dbus_reply_call( request_message, local_object, ) - except DbusFailedError as e: + except Exception as e: if not request_message.expect_reply: return + dbus_error = EXCEPTION_TO_DBUS_ERROR.get(type(e)) + if dbus_error is None: + dbus_error = DbusFailedError.dbus_error_name + error_message = request_message.create_error_reply( - e.dbus_error_name, + dbus_error, str(e.args[0]) if e.args else "", ) error_message.send() return - except Exception: - error_message = request_message.create_error_reply( - DbusFailedError.dbus_error_name, - "", - ) - error_message.send() - return if not request_message.expect_reply: return diff --git a/src/sdbus/sd_bus_internals.py b/src/sdbus/sd_bus_internals.py index d6746dd..1f3a0a0 100644 --- a/src/sdbus/sd_bus_internals.py +++ b/src/sdbus/sd_bus_internals.py @@ -304,9 +304,9 @@ class SdBusRequestNameAlreadyOwnerError(SdBusRequestNameError): ... -DBUS_ERROR_TO_EXCEPTION: dict[str, Exception] = {} +DBUS_ERROR_TO_EXCEPTION: dict[str, type[Exception]] = {} -EXCEPTION_TO_DBUS_ERROR: dict[Exception, str] = {} +EXCEPTION_TO_DBUS_ERROR: dict[type[Exception], str] = {} DbusDeprecatedFlag: int = 0 DbusHiddenFlag: int = 0 diff --git a/src/sdbus/sd_bus_internals_funcs.c b/src/sdbus/sd_bus_internals_funcs.c index 676d53e..b83e621 100644 --- a/src/sdbus/sd_bus_internals_funcs.c +++ b/src/sdbus/sd_bus_internals_funcs.c @@ -144,7 +144,7 @@ static PyObject* map_exception_to_dbus_error(PyObject* Py_UNUSED(self), PyObject #endif if (CALL_PYTHON_INT_CHECK(PyDict_Contains(dbus_error_to_exception_dict, dbus_error_string)) > 0) { - PyErr_Format(PyExc_ValueError, "Dbus error %R is already mapped.", dbus_error_string); + PyErr_Format(PyExc_ValueError, "D-Bus error %R is already mapped.", dbus_error_string); return NULL; } @@ -166,7 +166,7 @@ static PyObject* add_exception_mapping(PyObject* Py_UNUSED(self), PyObject* args PyObject* dbus_error_string CLEANUP_PY_OBJECT = CALL_PYTHON_AND_CHECK(PyObject_GetAttrString(exception, "dbus_error_name")); if (CALL_PYTHON_INT_CHECK(PyDict_Contains(dbus_error_to_exception_dict, dbus_error_string)) > 0) { - PyErr_Format(PyExc_ValueError, "Dbus error %R is already mapped.", dbus_error_string); + PyErr_Format(PyExc_ValueError, "D-Bus error %R is already mapped.", dbus_error_string); return NULL; } diff --git a/test/test_sdbus_async.py b/test/test_sdbus_async.py index f17b11a..db6b297 100644 --- a/test/test_sdbus_async.py +++ b/test/test_sdbus_async.py @@ -196,6 +196,10 @@ async def raise_and_unmap_error(self) -> None: raise DbusErrorUnmappedLater('Should be unmapped') + @dbus_method_async() + async def raise_python_exc(self) -> None: + raise ValueError("Test!") + @dbus_method_async('s', flags=DbusNoReplyFlag) async def no_reply_method(self, new_value: str) -> None: self.no_reply_sync.set() @@ -1004,3 +1008,12 @@ async def test() -> None: with self.assertRaisesRegex(RuntimeError, "different loop"): asyncio_run(test()) + + async def test_python_exc(self) -> None: + test_object, test_object_connection = initialize_object() + + with self.assertRaisesRegex(ValueError, "Test!"): + await test_object.raise_python_exc() + + with self.assertRaisesRegex(ValueError, "Test!"): + await test_object_connection.raise_python_exc() From 4dac6ca7249a6ae570943846e562e6f51bee0fe1 Mon Sep 17 00:00:00 2001 From: igo95862 Date: Fri, 7 Mar 2025 20:55:57 +0000 Subject: [PATCH 145/188] Use thread local storage for default bus instead of ContextVar The ContextVar switches contexts a lot even then bus is safe to use. For example, every ascynio task uses its own context even though they all run in the same thread. The bus is only unsafe then used in different threads. Move all default bus functions to `sdbus.default_bus`. **Removed awaiting on `request_default_bus_name`.** This raised deprecation warnings for several versions. Originally the removal was marked for version 1.0.0 but that version got delayed. --- DEPRECATIONS.md | 2 +- docs/common_api.rst | 72 ++------ src/sdbus/__init__.py | 21 ++- src/sdbus/__main__.py | 2 +- src/sdbus/dbus_common_elements.py | 2 +- src/sdbus/dbus_common_funcs.py | 90 +--------- src/sdbus/dbus_proxy_async_interface_base.py | 2 +- src/sdbus/dbus_proxy_async_object_manager.py | 2 +- src/sdbus/dbus_proxy_async_signal.py | 2 +- src/sdbus/default_bus.py | 172 +++++++++++++++++++ src/sdbus/unittest.py | 9 +- src/sdbus/utils/inspect.py | 2 +- test/test_deprecations.py | 14 -- 13 files changed, 213 insertions(+), 179 deletions(-) create mode 100644 src/sdbus/default_bus.py diff --git a/DEPRECATIONS.md b/DEPRECATIONS.md index 08ecdfd..0da80d0 100644 --- a/DEPRECATIONS.md +++ b/DEPRECATIONS.md @@ -8,7 +8,7 @@ function but returns an awaitable for backwards compatibility. * **Since**: 0.11.0 * **Warning**: 0.11.0 -* **Removed**: 1.0.0 +* **Removed**: 0.14.0 ## Importing exceptions from `sdbus` module diff --git a/docs/common_api.rst b/docs/common_api.rst index a604447..3c39f58 100644 --- a/docs/common_api.rst +++ b/docs/common_api.rst @@ -3,77 +3,37 @@ Common API These calls are shared between async and blocking API. -.. py:currentmodule:: sdbus - -D-Bus connections calls -++++++++++++++++++++++++++++++++++ - -.. py:function:: request_default_bus_name_async(new_name, allow_replacement, replace_existing, queue) - :async: - - Acquire a name on the default bus async. - - :param str new_name: the name to acquire. - Must be a valid D-Bus service name. - :param str new_name: the name to acquire. - Must be a valid D-Bus service name. - :param bool allow_replacement: If name was acquired allow other peers - to take away the name. - :param bool replace_existing: If current name owner allows, take - away the name. - :param bool queue: Queue up for name acquisition. - :py:exc:`.SdBusRequestNameInQueueError` will be raised when successfully - placed in queue. :py:meth:`Ownership change signal ` - should be monitored get notified when the name was acquired. - :raises: :ref:`name-request-exceptions` and other D-Bus exceptions. +Default bus ++++++++++++ -.. py:function:: request_default_bus_name(new_name, allow_replacement, replace_existing, queue) +.. automodule:: sdbus.default_bus + :members: - Acquire a name on the default bus. - - :param str new_name: the name to acquire. - Must be a valid D-Bus service name. - :param bool allow_replacement: If name was acquired allow other peers - to take away the name. - :param bool replace_existing: If current name owner allows, take - away the name. - :param bool queue: Queue up for name acquisition. - :py:exc:`.SdBusRequestNameInQueueError` will be raised when successfully - placed in queue. :py:meth:`Ownership change signal ` - should be monitored get notified when the name was acquired. - :raises: :ref:`name-request-exceptions` and other D-Bus exceptions. - -.. py:function:: set_default_bus(new_default) - - Sets default bus. - - Should be called before you create any objects that might use - default bus. - - Default bus can be replaced but the change will only affect - newly created objects. +.. py:currentmodule:: sdbus - :param SdBus new_default: The bus object to set default to. +D-Bus connections calls ++++++++++++++++++++++++ -.. py:function:: get_default_bus(new_default) +.. py:function:: sd_bus_open() - Gets default bus. + Opens a new bus connection. The session bus will be opened + when available or system bus otherwise. - :return: default bus + :return: Session or system bus. :rtype: SdBus .. py:function:: sd_bus_open_user() Opens a new user session bus connection. - :return: session bus + :return: Session bus. :rtype: SdBus .. py:function:: sd_bus_open_system() Opens a new system bus connection. - :return: system bus + :return: System bus. :rtype: SdBus .. py:function:: sd_bus_open_system_remote(host) @@ -84,7 +44,7 @@ D-Bus connections calls ``systemd-nspawn`` container name. :param str host: Host name to connect. - :return: Remote system bus + :return: Remote system bus. :rtype: SdBus .. py:function:: sd_bus_open_system_machine(machine) @@ -94,7 +54,7 @@ D-Bus connections calls Special machine name ``.host`` indicates local system. :param str machine: Machine (container) name. - :return: Remote system bus + :return: Remote system bus. :rtype: SdBus .. py:function:: sd_bus_open_user_machine(machine) @@ -104,7 +64,7 @@ D-Bus connections calls prefixed with ``username@`` for a specific user. :param str machine: Machine (container) name. - :return: Remote system bus + :return: Remote system bus. :rtype: SdBus Helper functions diff --git a/src/sdbus/__init__.py b/src/sdbus/__init__.py index 072f8c6..e4529ed 100644 --- a/src/sdbus/__init__.py +++ b/src/sdbus/__init__.py @@ -17,13 +17,8 @@ # 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 .dbus_common_funcs import ( - get_default_bus, - request_default_bus_name, - request_default_bus_name_async, - set_default_bus, -) from .dbus_exceptions import ( DbusAccessDeniedError, DbusAddressInUseError, @@ -75,6 +70,12 @@ ) from .dbus_proxy_sync_method import dbus_method from .dbus_proxy_sync_property import dbus_property +from .default_bus import ( + get_default_bus, + request_default_bus_name, + request_default_bus_name_async, + set_default_bus, +) from .sd_bus_internals import ( DbusDeprecatedFlag, DbusHiddenFlag, @@ -101,9 +102,6 @@ ) __all__ = ( - 'get_default_bus', 'request_default_bus_name', - 'request_default_bus_name_async', 'set_default_bus', - 'DbusAccessDeniedError', 'DbusAddressInUseError', 'DbusAuthFailedError', 'DbusBadAddressError', 'DbusDisconnectedError', 'DbusFailedError', @@ -144,6 +142,11 @@ 'dbus_property', + "get_default_bus", + "request_default_bus_name", + "request_default_bus_name_async", + "set_default_bus", + 'DbusDeprecatedFlag', 'DbusHiddenFlag', 'DbusNoReplyFlag', diff --git a/src/sdbus/__main__.py b/src/sdbus/__main__.py index a81ddc8..d9ad8ae 100644 --- a/src/sdbus/__main__.py +++ b/src/sdbus/__main__.py @@ -123,7 +123,7 @@ def run_gen_from_connection( from .dbus_proxy_sync_interfaces import DbusInterfaceCommon if system: - from .dbus_common_funcs import set_default_bus + from .default_bus import set_default_bus from .sd_bus_internals import sd_bus_open_system set_default_bus(sd_bus_open_system()) diff --git a/src/sdbus/dbus_common_elements.py b/src/sdbus/dbus_common_elements.py index ac49be1..703b9d0 100644 --- a/src/sdbus/dbus_common_elements.py +++ b/src/sdbus/dbus_common_elements.py @@ -24,9 +24,9 @@ from .dbus_common_funcs import ( _is_property_flags_correct, - get_default_bus, snake_case_to_camel_case, ) +from .default_bus import get_default_bus from .sd_bus_internals import is_interface_name_valid, is_member_name_valid if TYPE_CHECKING: diff --git a/src/sdbus/dbus_common_funcs.py b/src/sdbus/dbus_common_funcs.py index 77dfbb3..08a7f4c 100644 --- a/src/sdbus/dbus_common_funcs.py +++ b/src/sdbus/dbus_common_funcs.py @@ -1,4 +1,3 @@ - # SPDX-License-Identifier: LGPL-2.1-or-later # Copyright (C) 2020-2023 igo95862 @@ -20,29 +19,20 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA from __future__ import annotations -from asyncio import Future, get_running_loop -from contextvars import ContextVar +from asyncio import get_running_loop from typing import TYPE_CHECKING -from warnings import warn from .sd_bus_internals import ( DbusPropertyConstFlag, DbusPropertyEmitsChangeFlag, DbusPropertyEmitsInvalidationFlag, DbusPropertyExplicitFlag, - NameAllowReplacementFlag, - NameQueueFlag, - NameReplaceExistingFlag, - sd_bus_open, ) if TYPE_CHECKING: - from collections.abc import Generator, Iterator, Mapping + from collections.abc import Iterator, Mapping from typing import Any, Literal - from .sd_bus_internals import SdBus - -DEFAULT_BUS: ContextVar[SdBus] = ContextVar('DEFAULT_BUS') PROPERTY_FLAGS_MASK = ( DbusPropertyConstFlag | DbusPropertyEmitsChangeFlag | @@ -59,82 +49,6 @@ def _is_property_flags_correct(flags: int) -> bool: return (0 <= num_of_flag_bits <= 1) -def _prepare_request_name_flags( - allow_replacement: bool, - replace_existing: bool, - queue: bool, -) -> int: - return ( - (NameAllowReplacementFlag if allow_replacement else 0) - + - (NameReplaceExistingFlag if replace_existing else 0) - + - (NameQueueFlag if queue else 0) - ) - - -def get_default_bus() -> SdBus: - try: - return DEFAULT_BUS.get() - except LookupError: - new_bus = sd_bus_open() - DEFAULT_BUS.set(new_bus) - return new_bus - - -def set_default_bus(new_default: SdBus) -> None: - DEFAULT_BUS.set(new_default) - - -async def request_default_bus_name_async( - new_name: str, - allow_replacement: bool = False, - replace_existing: bool = False, - queue: bool = False, -) -> None: - default_bus = get_default_bus() - await default_bus.request_name_async( - new_name, - _prepare_request_name_flags( - allow_replacement, - replace_existing, - queue, - ) - ) - - -class _DeprecationAwaitable: - def __await__(self) -> Generator[Future[None], None, None]: - warn( - ( - 'Awaiting on request_default_bus_name' - 'is deprecated and will be removed.' - ), - DeprecationWarning, - ) - f: Future[None] = Future() - f.set_result(None) - yield from f - - -def request_default_bus_name( - new_name: str, - allow_replacement: bool = False, - replace_existing: bool = False, - queue: bool = False, -) -> _DeprecationAwaitable: - default_bus = get_default_bus() - default_bus.request_name( - new_name, - _prepare_request_name_flags( - allow_replacement, - replace_existing, - queue, - ) - ) - return _DeprecationAwaitable() - - def _snake_case_to_camel_case_gen(snake: str) -> Iterator[str]: char_iter = iter(snake) # Name starting with upper case letter diff --git a/src/sdbus/dbus_proxy_async_interface_base.py b/src/sdbus/dbus_proxy_async_interface_base.py index ffeba91..eccbf9e 100644 --- a/src/sdbus/dbus_proxy_async_interface_base.py +++ b/src/sdbus/dbus_proxy_async_interface_base.py @@ -39,13 +39,13 @@ DbusPropertyOverride, DbusRemoteObjectMeta, ) -from .dbus_common_funcs import get_default_bus from .dbus_proxy_async_method import DbusLocalMethodAsync, DbusMethodAsync from .dbus_proxy_async_property import ( DbusLocalPropertyAsync, DbusPropertyAsync, ) from .dbus_proxy_async_signal import DbusLocalSignalAsync, DbusSignalAsync +from .default_bus import get_default_bus from .sd_bus_internals import SdBusInterface if TYPE_CHECKING: diff --git a/src/sdbus/dbus_proxy_async_object_manager.py b/src/sdbus/dbus_proxy_async_object_manager.py index 584087e..9ed8063 100644 --- a/src/sdbus/dbus_proxy_async_object_manager.py +++ b/src/sdbus/dbus_proxy_async_object_manager.py @@ -23,7 +23,6 @@ from typing import TYPE_CHECKING from .dbus_common_elements import DbusLocalObjectMeta -from .dbus_common_funcs import get_default_bus from .dbus_proxy_async_interface_base import ( DbusExportHandle, DbusInterfaceBaseAsync, @@ -31,6 +30,7 @@ from .dbus_proxy_async_interfaces import DbusInterfaceCommonAsync from .dbus_proxy_async_method import dbus_method_async from .dbus_proxy_async_signal import dbus_signal_async +from .default_bus import get_default_bus if TYPE_CHECKING: from collections.abc import Callable diff --git a/src/sdbus/dbus_proxy_async_signal.py b/src/sdbus/dbus_proxy_async_signal.py index b425d49..e99df9b 100644 --- a/src/sdbus/dbus_proxy_async_signal.py +++ b/src/sdbus/dbus_proxy_async_signal.py @@ -33,7 +33,7 @@ DbusRemoteObjectMeta, DbusSignalCommon, ) -from .dbus_common_funcs import get_default_bus +from .default_bus import get_default_bus if TYPE_CHECKING: from collections.abc import Callable, Sequence diff --git a/src/sdbus/default_bus.py b/src/sdbus/default_bus.py new file mode 100644 index 0000000..fa9a89e --- /dev/null +++ b/src/sdbus/default_bus.py @@ -0,0 +1,172 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later + +# Copyright (C) 2020-2023 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 + +import threading +from logging import getLogger +from typing import TYPE_CHECKING + +from .sd_bus_internals import ( + NameAllowReplacementFlag, + NameQueueFlag, + NameReplaceExistingFlag, + sd_bus_open, +) + +if TYPE_CHECKING: + from typing import Optional + + from .sd_bus_internals import SdBus + +logger = getLogger(__name__) + + +class DefaultBusTLStorage(threading.local): + bus: Optional[SdBus] = None + + +bus_tls = DefaultBusTLStorage() + + +def _get_defaul_bus_tls() -> Optional[SdBus]: + return bus_tls.bus + + +def _set_default_bus_tls(new_bus: Optional[SdBus]) -> None: + bus_tls.bus = new_bus + + +def get_default_bus() -> SdBus: + """Get default thread-local bus.""" + current_bus = _get_defaul_bus_tls() + if current_bus is not None: + return current_bus + else: + new_bus = sd_bus_open() + logger.info( + "Created new default bus for thread %r", + threading.current_thread(), + ) + _set_default_bus_tls(new_bus) + return new_bus + + +def set_default_bus(new_default: SdBus) -> None: + """Set default thread-local bus. + + Should be called before creating any objects that will use + default bus. + + Default bus can be replaced but the change will only affect + newly created objects. + """ + _set_default_bus_tls(new_default) + + +def _prepare_request_name_flags( + allow_replacement: bool, + replace_existing: bool, + queue: bool, +) -> int: + return ( + (NameAllowReplacementFlag if allow_replacement else 0) + + + (NameReplaceExistingFlag if replace_existing else 0) + + + (NameQueueFlag if queue else 0) + ) + + +async def request_default_bus_name_async( + new_name: str, + allow_replacement: bool = False, + replace_existing: bool = False, + queue: bool = False, +) -> None: + r"""Asyncronously acquire a name on the default bus. + + :param new_name: + Name to acquire. + Must be a valid D-Bus service name. + :param allow_replacement: + If name was acquired allow other D-Bus peers to take away the name. + :param replace_existing: + If current name owner allows, take away the name. + :param queue: + Queue up for name acquisition. :py:exc:`.SdBusRequestNameInQueueError` + will be raised when successfully placed in queue. :py:meth:`Ownership + change signal ` should be monitored get notified when the name + was acquired. + :raises: :ref:`name-request-exceptions` and other D-Bus exceptions. + """ + default_bus = get_default_bus() + await default_bus.request_name_async( + new_name, + _prepare_request_name_flags( + allow_replacement, + replace_existing, + queue, + ) + ) + + +def request_default_bus_name( + new_name: str, + allow_replacement: bool = False, + replace_existing: bool = False, + queue: bool = False, +) -> None: + r"""Acquire a name on the default bus. + + Blocks until a reply is recieved from D-Bus daemon. + + :param new_name: + Name to acquire. + Must be a valid D-Bus service name. + :param allow_replacement: + If name was acquired allow other D-Bus peers to take away the name. + :param replace_existing: + If current name owner allows, take away the name. + :param queue: + Queue up for name acquisition. :py:exc:`.SdBusRequestNameInQueueError` + will be raised when successfully placed in queue. :py:meth:`Ownership + change signal ` should be monitored get notified when the name + was acquired. + :raises: :ref:`name-request-exceptions` and other D-Bus exceptions. + """ + default_bus = get_default_bus() + default_bus.request_name( + new_name, + _prepare_request_name_flags( + allow_replacement, + replace_existing, + queue, + ) + ) + + +__all__ = ( + "get_default_bus", + "set_default_bus", + "request_default_bus_name_async", + "request_default_bus_name", +) diff --git a/src/sdbus/unittest.py b/src/sdbus/unittest.py index b31a083..62ac280 100644 --- a/src/sdbus/unittest.py +++ b/src/sdbus/unittest.py @@ -32,8 +32,8 @@ from unittest import IsolatedAsyncioTestCase from weakref import ref as weak_ref -from .dbus_common_funcs import set_default_bus from .dbus_proxy_async_signal import DbusLocalSignalAsync, DbusProxySignalAsync +from .default_bus import _get_defaul_bus_tls, _set_default_bus_tls from .sd_bus_internals import SdBusMessage, sd_bus_open_user if TYPE_CHECKING: @@ -214,8 +214,10 @@ def _isolated_dbus( ) environ["DBUS_SESSION_BUS_ADDRESS"] = f"unix:path={dbus_socket_path}" + old_bus = _get_defaul_bus_tls() bus = sd_bus_open_user() - set_default_bus(bus) + _set_default_bus_tls(bus) + exit_stack.callback(_set_default_bus_tls, old_bus) yield bus @@ -226,9 +228,6 @@ def setUp(self) -> None: self.bus = _isolated_dbus_cm.__enter__() self.addCleanup(_isolated_dbus_cm.__exit__, None, None, None) - async def asyncSetUp(self) -> None: - set_default_bus(self.bus) - def assertDbusSignalEmits( self, signal: DbusBoundSignalAsyncBase[Any], diff --git a/src/sdbus/utils/inspect.py b/src/sdbus/utils/inspect.py index 3aecdf8..92dce77 100644 --- a/src/sdbus/utils/inspect.py +++ b/src/sdbus/utils/inspect.py @@ -22,9 +22,9 @@ from typing import TYPE_CHECKING from ..dbus_common_elements import DbusLocalObjectMeta, DbusRemoteObjectMeta -from ..dbus_common_funcs import get_default_bus from ..dbus_proxy_async_interface_base import DbusInterfaceBaseAsync from ..dbus_proxy_sync_interface_base import DbusInterfaceBase +from ..default_bus import get_default_bus if TYPE_CHECKING: from typing import Optional, Union diff --git a/test/test_deprecations.py b/test/test_deprecations.py index 702fa83..aec99ee 100644 --- a/test/test_deprecations.py +++ b/test/test_deprecations.py @@ -21,19 +21,5 @@ from unittest import main -from sdbus.unittest import IsolatedDbusTestCase - -from sdbus import request_default_bus_name - - -class TestDeprecations(IsolatedDbusTestCase): - async def test_await_on_blocking_request_name(self) -> None: - with self.assertWarnsRegex( - DeprecationWarning, - 'Awaiting on request_default_bus_name' - ): - await request_default_bus_name('org.example.test') - - if __name__ == '__main__': main() From b44d56519f96a61fcfde26600f1ab38daa7e2647 Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sun, 30 Mar 2025 15:51:24 +0100 Subject: [PATCH 146/188] New export_to_dbus algorithm using metadata Instead of iterating over all members to find D-Bus elements use the `_dbus_iter_interfaces_meta` to iterate over the metadata and then `getattr` the exact Python attributes containing the D-Bus elements. This makes the algorithm faster as it won't have to go through all members as well as allows for D-Bus interfaces without any members to be exported. Raise an error if no interfaces got exported to prevent unexpected errors except for ObjectManager where libsystemd allow exporting without any extra interfaces. --- src/sdbus/dbus_proxy_async_interface_base.py | 53 +++++++------------- src/sdbus/dbus_proxy_async_object_manager.py | 3 ++ test/test_sdbus_async.py | 14 ++++++ test/test_sdbus_utils.py | 2 +- 4 files changed, 36 insertions(+), 36 deletions(-) diff --git a/src/sdbus/dbus_proxy_async_interface_base.py b/src/sdbus/dbus_proxy_async_interface_base.py index eccbf9e..e0b6f5f 100644 --- a/src/sdbus/dbus_proxy_async_interface_base.py +++ b/src/sdbus/dbus_proxy_async_interface_base.py @@ -21,7 +21,6 @@ from collections.abc import Callable from copy import copy -from inspect import getmembers from itertools import chain from types import MethodType from typing import TYPE_CHECKING, Any, cast @@ -52,7 +51,6 @@ from collections.abc import Iterable, Iterator from typing import Optional, TypeVar, Union - from .dbus_common_elements import DbusBoundAsync from .sd_bus_internals import SdBus, SdBusSlot T = TypeVar('T') @@ -312,12 +310,14 @@ async def start_serving(self, DeprecationWarning) self.export_to_dbus(object_path, bus) + def _dbus_on_no_members_exported(self) -> None: + raise ValueError("No D-Bus interfaces were exported") + def export_to_dbus( self, object_path: str, bus: Optional[SdBus] = None, ) -> DbusExportHandle: - local_object_meta = self._dbus if isinstance(local_object_meta, DbusRemoteObjectMeta): raise RuntimeError("Cannot export D-Bus proxies.") @@ -334,38 +334,17 @@ def export_to_dbus( local_object_meta.attached_bus = bus local_object_meta.serving_object_path = object_path - # TODO: can be optimized with a single loop - interface_map: dict[str, list[DbusBoundAsync]] = {} - - for key, value in getmembers(self): - assert not isinstance(value, DbusMemberAsync) - - if isinstance(value, DbusLocalMethodAsync): - interface_name = value.dbus_method.interface_name - if not value.dbus_method.serving_enabled: - continue - elif isinstance(value, DbusLocalPropertyAsync): - interface_name = value.dbus_property.interface_name - if not value.dbus_property.serving_enabled: - continue - elif isinstance(value, DbusLocalSignalAsync): - interface_name = value.dbus_signal.interface_name - if not value.dbus_signal.serving_enabled: - continue - else: - continue - - try: - interface_member_list = interface_map[interface_name] - except KeyError: - interface_member_list = [] - interface_map[interface_name] = interface_member_list - interface_member_list.append(value) + for interface_name, meta in self._dbus_iter_interfaces_meta(): + if not meta.serving_enabled: + continue - for interface_name, member_list in interface_map.items(): new_interface = SdBusInterface() - for dbus_something in member_list: + + for python_attr, dbus_member in ( + meta.python_attr_to_dbus_member.items() + ): + dbus_something = getattr(self, python_attr) if isinstance(dbus_something, DbusLocalMethodAsync): new_interface.add_method( dbus_something.dbus_method.method_name, @@ -404,12 +383,16 @@ def export_to_dbus( dbus_something.dbus_signal.flags, ) else: - raise TypeError + raise TypeError( + "Expected D-Bus element, got: {dbus_something!r}" + ) - bus.add_interface(new_interface, object_path, - interface_name) + bus.add_interface(new_interface, object_path, interface_name) local_object_meta.activated_interfaces.append(new_interface) + if not local_object_meta.activated_interfaces: + self._dbus_on_no_members_exported() + return DbusExportHandle(local_object_meta) def _connect( diff --git a/src/sdbus/dbus_proxy_async_object_manager.py b/src/sdbus/dbus_proxy_async_object_manager.py index 9ed8063..d252662 100644 --- a/src/sdbus/dbus_proxy_async_object_manager.py +++ b/src/sdbus/dbus_proxy_async_object_manager.py @@ -76,6 +76,9 @@ def interfaces_added(self) -> tuple[str, dict[str, dict[str, Any]]]: def interfaces_removed(self) -> tuple[str, list[str]]: raise NotImplementedError + def _dbus_on_no_members_exported(self) -> None: + ... # Object manager is allowed to be exported empty + def export_to_dbus( self, object_path: str, diff --git a/test/test_sdbus_async.py b/test/test_sdbus_async.py index db6b297..f5b2ebb 100644 --- a/test/test_sdbus_async.py +++ b/test/test_sdbus_async.py @@ -1017,3 +1017,17 @@ async def test_python_exc(self) -> None: with self.assertRaisesRegex(ValueError, "Test!"): await test_object_connection.raise_python_exc() + + async def test_empty_dbus_interface(self) -> None: + class Empty( + DbusInterfaceCommonAsync, + interface_name="org.empty", + ): + ... + + empty_local = Empty() + empty_local.export_to_dbus("/") + empty_proxy = Empty.new_proxy(TEST_SERVICE_NAME, "/") + + intro = await empty_proxy.dbus_introspect() + self.assertIn('', intro) diff --git a/test/test_sdbus_utils.py b/test/test_sdbus_utils.py index c9fa774..608ea68 100644 --- a/test/test_sdbus_utils.py +++ b/test/test_sdbus_utils.py @@ -212,7 +212,7 @@ def test_inspect_dbus_path_async_proxy(self) -> None: inspect_dbus_path(proxy, new_bus) def test_inspect_dbus_path_async_local(self) -> None: - local_obj = DbusInterfaceCommonAsync() + local_obj = FooBarAsync() with self.assertRaisesRegex( LookupError, "is not exported to any D-Bus", From 67a0001c4923c59cfef94bd4a10e4531ac88ca32 Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sun, 30 Mar 2025 15:59:55 +0100 Subject: [PATCH 147/188] Fix unused nonlocal in test_sdbus_async.py Looks like new version of pyflake can detect unused nonlocal. --- test/test_sdbus_async.py | 1 - 1 file changed, 1 deletion(-) diff --git a/test/test_sdbus_async.py b/test/test_sdbus_async.py index f5b2ebb..ccf341c 100644 --- a/test/test_sdbus_async.py +++ b/test/test_sdbus_async.py @@ -374,7 +374,6 @@ def test_property(self) -> str: @test_property.setter def test_property_setter(self, var: str) -> None: - nonlocal test_var test_var.insert(0, var) test_subclass = TestInheritence() From 4e40694987598fdf3c2256f02674e7a6179258ea Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sun, 30 Mar 2025 20:46:43 +0100 Subject: [PATCH 148/188] wheel-build: Start on a new build script * Use Debian 11 as it provides libmount.a and libcap.a * Only compile libsystemd.a and libsystemd.pc and manually install the files. * Patch systemd to make it compatible with glibc 2.28. * Debian will allow ARMv7l binary packages. --- wheel-build/run_podman_full_build.py | 318 ++++++++++++++++++ .../systemd_no_gettid_no_getdents64.patch | 30 ++ 2 files changed, 348 insertions(+) create mode 100644 wheel-build/run_podman_full_build.py create mode 100644 wheel-build/systemd_no_gettid_no_getdents64.patch diff --git a/wheel-build/run_podman_full_build.py b/wheel-build/run_podman_full_build.py new file mode 100644 index 0000000..e6cf825 --- /dev/null +++ b/wheel-build/run_podman_full_build.py @@ -0,0 +1,318 @@ +# 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 collections.abc import Callable, Iterator +from functools import partial +from pathlib import Path +from subprocess import PIPE +from subprocess import run as _run + +SDBUS_REFSPEC = "HEAD" +SDBUS_SRC_DIR = Path("/root/sdbus") + +WHEEL_BUILD_DIR = Path(__file__).parent +PROJECT_ROOT = WHEEL_BUILD_DIR.parent +BUILD_DIR = PROJECT_ROOT / "build/wheel-build/" +LAST_STAGE_FILE = BUILD_DIR / "last_stage" + +CONTAINER_IMAGE = "debian:11-slim" +CONTAINER_NAME = "python-sdbus-build" +CONTAINER_ARCH = "x86_64" +DEBIAN_PACKAGES = ( + "python3", + "python3-dev", + "gcc", "gperf", + "meson", + "python3-wheel", + "python-setuptools", + "python3-jinja2", + "libcap-dev", + "libmount-dev", + "git", + "ca-certificates", + "pkg-config", +) +DEBIAN_NAME = "bullseye" + +BASIC_CFLAGS: list[str] = [ + '-O2', '-fno-plt', '-D_FORTIFY_SOURCE=2', + '-fstack-clash-protection', +] + +SYSTEMD_REPO = "https://github.com/systemd/systemd-stable.git" +# systemd 255 is last one before glibc 2.31 requirement +SYSTEMD_TAG = "v255.18" +SYSTEMD_SRC_DIR = Path("/root/systemd") +SYSTEMD_BUILD_DIR = SYSTEMD_SRC_DIR / "build" +SYSTEMD_COMPAT_PATCH_NAME = "systemd_no_gettid_no_getdents64.patch" +SYSTEMD_COMPAT_PATCH_FILE = WHEEL_BUILD_DIR / SYSTEMD_COMPAT_PATCH_NAME +SYSTEMD_OPTIONS: list[str] = [ + "static-libsystemd=pic", + "tests=false", + "coredump=false", + "dbus=false", + "efi=false", + "elfutils=false", + "hostnamed=false", + "homed=false", + "importd=false", + "initrd=false", + "kernel-install=false", + "logind=false", + "machined=false", + "man=false", + "networkd=false", + "portabled=false", + "repart=false", + "sysext=false", + "sysusers=false", + "timedated=false", + "timesyncd=false", + "tmpfiles=false", + "oomd=false", + "hibernate=false", + "nss-systemd=false", + "nss-resolve=false", +] + +run = partial(_run, check=True, cwd=PROJECT_ROOT) + + +def podman_exec( + *args: str, + env: dict[str, str] | None = None, + cwd: Path | None = None, + input: bytes | None = None, +) -> None: + + env_list = [ + f"--env={env_k}={env_v}" for env_k, env_v in env.items() + ] if env else [] + workdir_options = [f"--workdir={cwd}"] if cwd else [] + + run( + args=( + "podman", + "exec", + *env_list, + *workdir_options, + "--tty" if input is None else "--interactive", + CONTAINER_NAME, + *args, + ), + input=input, + ) + + +def podman_cp(src: Path, dest: Path) -> None: + run( + args=("podman", "cp", str(src.absolute()), f"{CONTAINER_NAME}:{dest}") + ) + + +def podman_start() -> None: + run( + args=( + "podman", + "run", + "--name", CONTAINER_NAME, + "--arch", CONTAINER_ARCH, + "--detach", + "--rm", "--init", + CONTAINER_IMAGE, + "sleep", "3d", + ) + ) + + +def install_packages() -> None: + target_release = ("--target-release", f"{DEBIAN_NAME}-backports") + deb_env = {"DEBIAN_FRONTEND": "noninteractive"} + podman_exec( + "bash", + "-c", + f"echo 'deb http://deb.debian.org/debian {DEBIAN_NAME}-backports main'" + " > /etc/apt/sources.list.d/backports.list" + ) + podman_exec("apt-get", "update", env=deb_env) + podman_exec( + "apt-get", + "upgrade", + *target_release, + "--yes", + env=deb_env, + ) + podman_exec( + "apt-get", + "install", + *target_release, + "--yes", + "--no-install-recommends", + *DEBIAN_PACKAGES, + env=deb_env, + ) + + +def clone_systemd() -> None: + podman_exec( + "git", "clone", + "--depth", "1", + "--branch", SYSTEMD_TAG, + "--", + SYSTEMD_REPO, + str(SYSTEMD_SRC_DIR), + ) + + +def apply_systemd_patch() -> None: + podman_cp(SYSTEMD_COMPAT_PATCH_FILE, SYSTEMD_SRC_DIR) + podman_exec( + "git", "apply", SYSTEMD_COMPAT_PATCH_NAME, + cwd=SYSTEMD_SRC_DIR, + ) + + +def build_systemd() -> None: + systemd_options_get = (f"-D{o}" for o in SYSTEMD_OPTIONS) + cflags = {"CFLAGS": " ".join(BASIC_CFLAGS)} + podman_exec( + "meson", + "setup", + "--auto-features=disabled", + "--buildtype=release", + *systemd_options_get, + str(SYSTEMD_BUILD_DIR), + cwd=SYSTEMD_SRC_DIR, + env=cflags, + ) + podman_exec( + "meson", + "compile", + "systemd:static_library", + "libsystemd.pc", + cwd=SYSTEMD_BUILD_DIR, + ) + + +def install_systemd_files() -> None: + podman_exec( + "bash", + "-c", + "cp libsystemd.a" + " /usr/lib/$(cat /usr/lib/pkg-config.multiarch)/", + cwd=SYSTEMD_BUILD_DIR, + ) + podman_exec( + "cp", + "src/libsystemd/libsystemd.pc", + "/usr/share/pkgconfig/", + cwd=SYSTEMD_BUILD_DIR, + ) + podman_exec( + "cp", + "src/libsystemd/libsystemd.pc", + "/usr/share/pkgconfig/", + cwd=SYSTEMD_BUILD_DIR, + ) + podman_exec( + "mkdir", "--parents", "/usr/include/systemd/" + ) + required_headers = ( + "_sd-common.h", + "sd-id128.h", + "sd-daemon.h", + "sd-bus.h", + "sd-bus-vtable.h", + "sd-bus-protocol.h", + "sd-device.h", + "sd-event.h", + ) + podman_exec( + "cp", + *(f"src/systemd/{h}" for h in required_headers), + "/usr/include/systemd/", + cwd=SYSTEMD_SRC_DIR, + ) + + +def copy_sdbus_sources() -> None: + podman_exec("mkdir", "--parents", str(SDBUS_SRC_DIR)) + sdbus_tar = run( + args=("git", "archive", SDBUS_REFSPEC), + stdout=PIPE, + ).stdout + assert isinstance(sdbus_tar, bytes) + print("python-sdbus source archive size:", len(sdbus_tar)) + podman_exec( + "tar", "--extract", "--verbose", + cwd=SDBUS_SRC_DIR, + input=sdbus_tar, + ) + + +def compile_sdbus() -> None: + podman_exec( + "python3", "setup.py", "build", "bdist_wheel", + cwd=SDBUS_SRC_DIR, + env={ + "PYTHON_SDBUS_USE_STATIC_LINK": "1", + "PYTHON_SDBUS_USE_LIMITED_API": "1", + }, + ) + + +STAGES: dict[str, Callable[[], None]] = { + "podman_start": podman_start, + "install_packages": install_packages, + "clone_systemd": clone_systemd, + "apply_systemd_patch": apply_systemd_patch, + "build_systemd": build_systemd, + "install_systemd_files": install_systemd_files, + "copy_sdbus_sources": copy_sdbus_sources, + "compile_sdbus": compile_sdbus, +} + + +def iter_stages() -> Iterator[tuple[str, Callable[[], None]]]: + stages_iter = iter(STAGES.items()) + if LAST_STAGE_FILE.exists(): + last_stage = LAST_STAGE_FILE.read_text().strip() + for stage_name, _ in stages_iter: + if stage_name == last_stage: + print("Last completed stage:", stage_name) + break + else: + print("Skipping stage:", stage_name) + + yield from stages_iter + + +def main() -> None: + BUILD_DIR.mkdir(parents=True, exist_ok=True) + + for stage_name, stage_func in iter_stages(): + stage_func() + LAST_STAGE_FILE.write_text(stage_name) + print("Completed:", stage_name) + + +if __name__ == "__main__": + main() diff --git a/wheel-build/systemd_no_gettid_no_getdents64.patch b/wheel-build/systemd_no_gettid_no_getdents64.patch new file mode 100644 index 0000000..138e2d9 --- /dev/null +++ b/wheel-build/systemd_no_gettid_no_getdents64.patch @@ -0,0 +1,30 @@ +diff --git a/meson.build b/meson.build +index 8c16c1c5c0..f34e4a0c3a 100644 +--- a/meson.build ++++ b/meson.build +@@ -573,8 +573,6 @@ endforeach + + foreach ident : [ + ['memfd_create', '''#include '''], +- ['gettid', '''#include +- #include '''], + ['fchmodat2', '''#include + #include '''], # no known header declares fchmodat2 + ['pivot_root', '''#include +@@ -631,13 +629,15 @@ foreach ident : [ + ['fsopen', '''#include '''], + ['fsconfig', '''#include '''], + ['fsmount', '''#include '''], +- ['getdents64', '''#include '''], + ] + + have = cc.has_function(ident[0], prefix : ident[1], args : '-D_GNU_SOURCE') + conf.set10('HAVE_' + ident[0].to_upper(), have) + endforeach + ++conf.set10('HAVE_GETTID', false) ++conf.set10('HAVE_GETDENTS64', false) ++ + if cc.has_function('getrandom', prefix : '''#include ''', args : '-D_GNU_SOURCE') + conf.set10('USE_SYS_RANDOM_H', true) + conf.set10('HAVE_GETRANDOM', true) From 0939b254cff718c38358d0fa3b61cc29631d3e5b Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sat, 5 Apr 2025 17:45:56 +0100 Subject: [PATCH 149/188] Add use_interface_subsets option to interface parsing functions When enabled the interface subset will be considered a valid match. Requested by @christophehenry. --- src/sdbus/utils/parse.py | 46 ++++++++++++++++++++++++++++++++++++---- test/test_sdbus_utils.py | 31 +++++++++++++++++++++++++++ 2 files changed, 73 insertions(+), 4 deletions(-) diff --git a/src/sdbus/utils/parse.py b/src/sdbus/utils/parse.py index 6138311..f56e5fe 100644 --- a/src/sdbus/utils/parse.py +++ b/src/sdbus/utils/parse.py @@ -44,7 +44,7 @@ ] InterfacesToClassMap = dict[ frozenset[str], - type[Union[DbusInterfaceBaseAsync, DbusInterfaceBase]], + InterfacesBaseTypes, ] OnUnknownMember = Literal['error', 'ignore', 'reuse'] OnUnknownInterface = Literal['error', 'none'] @@ -72,9 +72,9 @@ def _interfaces_input_to_types( def parse_properties_changed( - interface: InterfacesInputElements, - properties_changed_data: DBUS_PROPERTIES_CHANGED_TYPING, - on_unknown_member: OnUnknownMember = 'error', + interface: InterfacesInputElements, + properties_changed_data: DBUS_PROPERTIES_CHANGED_TYPING, + on_unknown_member: OnUnknownMember = 'error', ) -> dict[str, Any]: """Parse data from :py:meth:`properties_changed \ ` signal. @@ -146,8 +146,19 @@ def _get_class_from_interfaces( interfaces_to_class_map: InterfacesToClassMap, interface_names_iter: Iterable[str], raise_key_error: bool, + use_subset: bool, ) -> Optional[InterfacesBaseTypes]: class_set = frozenset(interface_names_iter) - SKIP_INTERFACES + if use_subset: + for interface_available in sorted( + interfaces_to_class_map.keys(), + key=len, + reverse=True, + ): + if interface_available.issubset(class_set): + class_set = interface_available + break + try: return interfaces_to_class_map[class_set] except KeyError: @@ -196,6 +207,8 @@ def parse_interfaces_added( interfaces_added_data: tuple[str, dict[str, dict[str, Any]]], on_unknown_interface: OnUnknownInterface = 'error', on_unknown_member: OnUnknownMember = 'error', + *, + use_interface_subsets: bool = False, ) -> tuple[str, Optional[InterfacesBaseTypes], dict[str, Any]]: """Parse data from :py:meth:`interfaces_added \ ` signal. @@ -220,6 +233,12 @@ def parse_interfaces_added( If an unknown D-Bus property was encountered either raise an ``"error"`` (default), ``"ignore"`` the property or ``"reuse"`` the D-Bus name for the member. + :param use_interface_subsets: + Use the subset of interfaces as a valid match. For example, + the class that implements ``org.example.foo`` would be matched + with an data consising of both ``org.example.foo`` and + ``org.example.bar``. The classes implementing more interfaces + will have higher priority over the ones implementing fewer. :returns: Path of new added object, object's class (or ``None``) and dictionary of python translated members and their values. @@ -234,6 +253,7 @@ def parse_interfaces_added( interfaces_to_class_map, properties_data.keys(), on_unknown_interface == "error", + use_interface_subsets, ) ) dbus_to_python_member_map = _get_member_map_from_class(python_class) @@ -265,6 +285,8 @@ def parse_interfaces_removed( interfaces: InterfacesInput, interfaces_removed_data: tuple[str, list[str]], on_unknown_interface: OnUnknownInterface = 'error', + *, + use_interface_subsets: bool = False, ) -> tuple[str, Optional[InterfacesBaseTypes]]: """Parse data from :py:meth:`interfaces_added \ ` signal. @@ -283,6 +305,12 @@ def parse_interfaces_removed( :param on_unknown_member: If an unknown D-Bus interface was encountered either raise an ``"error"`` (default) or return ``"none"`` instead of interface class. + :param use_interface_subsets: + Use the subset of interfaces as a valid match. For example, + the class that implements ``org.example.foo`` would be matched + with an data consising of both ``org.example.foo`` and + ``org.example.bar``. The classes implementing more interfaces + will have higher priority over the ones implementing fewer. :returns: Path of removed object and object's class (or ``None``). """ @@ -296,6 +324,7 @@ def parse_interfaces_removed( interfaces_to_class_map, interfaces_removed, on_unknown_interface == "error", + use_interface_subsets, ) ) @@ -307,6 +336,8 @@ def parse_get_managed_objects( managed_objects_data: dict[str, dict[str, dict[str, Any]]], on_unknown_interface: OnUnknownInterface = 'error', on_unknown_member: OnUnknownMember = 'error', + *, + use_interface_subsets: bool = False, ) -> ParseGetManaged: """Parse data from :py:meth:`get_managed_objects \ ` call. @@ -330,6 +361,12 @@ def parse_get_managed_objects( If an unknown D-Bus property was encountered either raise an ``"error"`` (default), ``"ignore"`` the property or ``"reuse"`` the D-Bus name for the member. + :param use_interface_subsets: + Use the subset of interfaces as a valid match. For example, + the class that implements ``org.example.foo`` would be matched + with an data consising of both ``org.example.foo`` and + ``org.example.bar``. The classes implementing more interfaces + will have higher priority over the ones implementing fewer. :returns: Dictionary where keys are paths and values are tuples of managed objects classes and their properties data. @@ -347,6 +384,7 @@ def parse_get_managed_objects( interfaces_to_class_map, properties_data.keys(), on_unknown_interface == "error", + use_interface_subsets, ) ) dbus_to_python_member_map = _get_member_map_from_class(python_class) diff --git a/test/test_sdbus_utils.py b/test/test_sdbus_utils.py index 608ea68..68b59d4 100644 --- a/test/test_sdbus_utils.py +++ b/test/test_sdbus_utils.py @@ -189,6 +189,37 @@ def test_parse_get_managed_objects_unknown_member_skip(self) -> None: self.assertIsNone(class_type) self.assertEqual(0, len(properties_data)) + def test_parse_get_managed_objects_interface_subset_single(self) -> None: + parsed_managed = parse_get_managed_objects( + [FooAsync], + MANAGED_OBJECTS_COMBINED, + on_unknown_interface="error", + on_unknown_member="reuse", + use_interface_subsets=True, + ) + class_type, properties_data = parsed_managed["/test"] + self.assertIs(class_type, FooAsync) + self.assertEqual(properties_data["foo"], 1) + self.assertEqual(properties_data["Bar"], 2) + + def test_parse_get_managed_objects_interface_subset_multiple(self) -> None: + parsed_managed = parse_get_managed_objects( + [FooAsync, FooBarAsync], + # FooBarAsync should be prioritized then both interfaces + # are available on the path. + MANAGED_OBJECTS_BOTH, + on_unknown_interface="none", + on_unknown_member="reuse", + use_interface_subsets=True, + ) + + class_type, _ = parsed_managed["/test"] + self.assertIs(class_type, FooBarAsync) + class_type, _ = parsed_managed["/foo"] + self.assertIs(class_type, FooAsync) + class_type, _ = parsed_managed["/bar"] + self.assertIsNone(class_type) + class TestSdbusUtilsInspect(IsolatedDbusTestCase): def test_inspect_dbus_path_block(self) -> None: From 227ac90a87308ef7243d697975e97094bdd8356c Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sat, 5 Apr 2025 18:07:30 +0100 Subject: [PATCH 150/188] test: Fix some tests not being skipped when jinja is not installed --- test/test_interface_generator.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/test/test_interface_generator.py b/test/test_interface_generator.py index 7156810..b53b49c 100644 --- a/test/test_interface_generator.py +++ b/test/test_interface_generator.py @@ -202,10 +202,13 @@ def test_parsing(self) -> None: class TestGeneratorAgainstDbus(IsolatedDbusTestCase): - def test_generate_from_connection(self) -> None: + def setUp(self) -> None: if find_spec('jinja2') is None: raise SkipTest('Jinja2 not installed') + super().setUp() + + def test_generate_from_connection(self) -> None: with patch("sdbus.__main__.stdout") as stdout_mock: generator_main( [ @@ -234,9 +237,6 @@ def test_generate_from_connection(self) -> None: ) def test_generate_from_connection_blocking(self) -> None: - if find_spec('jinja2') is None: - raise SkipTest('Jinja2 not installed') - with patch("sdbus.__main__.stdout") as stdout_mock: generator_main( [ @@ -275,6 +275,12 @@ def test_generate_from_connection_blocking(self) -> None: class TestGeneratorSyntaxCompile(TestCase): + def setUp(self) -> None: + if find_spec('jinja2') is None: + raise SkipTest('Jinja2 not installed') + + super().setUp() + def test_syntax_compile_async(self) -> None: source_code = generate_py_file( interfaces_from_str(test_xml), From fd98d9b6614ffe50a6dec9e8069ef5f5db61673e Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sat, 5 Apr 2025 18:38:07 +0100 Subject: [PATCH 151/188] Use Python 3.9 generic collections type hints in generated code Since Python 3.9 is now a minimal version the `list`, `dict` and `tuple` type hints can now be used instead of `typing.List`... --- src/sdbus/interface_generator.py | 8 ++++---- test/test_interface_generator.py | 16 ++++++++-------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/sdbus/interface_generator.py b/src/sdbus/interface_generator.py index de26c7c..0c87b8b 100644 --- a/src/sdbus/interface_generator.py +++ b/src/sdbus/interface_generator.py @@ -136,7 +136,7 @@ def typing_basic(cls, char: str) -> str: @staticmethod def typing_into_tuple(typing_iter: Iterable[str]) -> str: - return f"Tuple[{', '.join(typing_iter)}]" + return f"tuple[{', '.join(typing_iter)}]" @staticmethod def slice_container(dbus_sig_iter: Iterator[str], peek_str: str) -> str: @@ -219,7 +219,7 @@ def typing_complete(cls, complete_sig: str) -> str: dict_value_sig = complete_sig[3:-1] dict_value_typing = cls.typing_complete(dict_value_sig) - return f"Dict[{dict_key_typing}, {dict_value_typing}]" + return f"dict[{dict_key_typing}, {dict_value_typing}]" elif complete_sig.startswith('a'): array_completes = cls.split_sig(complete_sig[1:]) @@ -230,7 +230,7 @@ def typing_complete(cls, complete_sig: str) -> str: array_single_complete = array_completes[0] - return f"List[{cls.typing_complete(array_single_complete)}]" + return f"list[{cls.typing_complete(array_single_complete)}]" elif complete_sig.startswith('('): if complete_sig[-1] != ')': raise ValueError(f"Malformed struct {complete_sig}") @@ -618,7 +618,7 @@ def has_members(self) -> bool: "generic_header": """\ from __future__ import annotations -from typing import Any, Dict, List, Tuple +from typing import Any """, "async_imports_header": """from sdbus import ( diff --git a/test/test_interface_generator.py b/test/test_interface_generator.py index b53b49c..5d072c0 100644 --- a/test/test_interface_generator.py +++ b/test/test_interface_generator.py @@ -112,7 +112,7 @@ def test_signature_to_typing(self) -> None: with self.subTest('Parse variant'): self.assertEqual( - 'Tuple[str, Any]', DbusSigToTyping.typing_complete('v') + 'tuple[str, Any]', DbusSigToTyping.typing_complete('v') ) with self.subTest('Splitter test'): @@ -124,40 +124,40 @@ def test_signature_to_typing(self) -> None: with self.subTest('Parse struct'): self.assertEqual( DbusSigToTyping.typing_complete('(sx)'), - 'Tuple[str, int]', + 'tuple[str, int]', ) with self.subTest('Parse list'): self.assertEqual( DbusSigToTyping.typing_complete('a(sx)'), - 'List[Tuple[str, int]]', + 'list[tuple[str, int]]', ) with self.subTest('Parse dict'): self.assertEqual( DbusSigToTyping.typing_complete('a{s(xh)}'), - 'Dict[str, Tuple[int, int]]', + 'dict[str, tuple[int, int]]', ) with self.subTest('Parse signature'): self.assertEqual( DbusSigToTyping.sig_to_typing('a{s(xh)}'), - 'Dict[str, Tuple[int, int]]', + 'dict[str, tuple[int, int]]', ) self.assertEqual( DbusSigToTyping.sig_to_typing('a{s(xh)}xs'), - 'Tuple[Dict[str, Tuple[int, int]], int, str]', + 'tuple[dict[str, tuple[int, int]], int, str]', ) self.assertEqual( DbusSigToTyping.sig_to_typing('a{s(xh)}xs'), - 'Tuple[Dict[str, Tuple[int, int]], int, str]', + 'tuple[dict[str, tuple[int, int]], int, str]', ) self.assertEqual( DbusSigToTyping.sig_to_typing('as'), - 'List[str]', + 'list[str]', ) self.assertEqual( From f88fcbe414ee424d00364a23aa192e710c735d0e Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sat, 5 Apr 2025 19:50:11 +0100 Subject: [PATCH 152/188] wheel-build: Finalize the new build script Add wheel copy command and `--arch` argument to build for a specific arch. --- wheel-build/run_podman_full_build.py | 33 +++++++++++++++++++++++++--- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/wheel-build/run_podman_full_build.py b/wheel-build/run_podman_full_build.py index e6cf825..8bae78e 100644 --- a/wheel-build/run_podman_full_build.py +++ b/wheel-build/run_podman_full_build.py @@ -19,6 +19,7 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA from __future__ import annotations +from argparse import ArgumentParser from collections.abc import Callable, Iterator from functools import partial from pathlib import Path @@ -33,7 +34,7 @@ BUILD_DIR = PROJECT_ROOT / "build/wheel-build/" LAST_STAGE_FILE = BUILD_DIR / "last_stage" -CONTAINER_IMAGE = "debian:11-slim" +CONTAINER_IMAGE = "docker.io/debian:11-slim" CONTAINER_NAME = "python-sdbus-build" CONTAINER_ARCH = "x86_64" DEBIAN_PACKAGES = ( @@ -122,9 +123,16 @@ def podman_exec( ) -def podman_cp(src: Path, dest: Path) -> None: +def podman_cp(src: Path, dest: Path, to_contatiner: bool = True) -> None: + if to_contatiner: + src_str = str(src.absolute()) + dest_str = f"{CONTAINER_NAME}:{dest}" + else: + src_str = f"{CONTAINER_NAME}:{src}" + dest_str = str(dest.absolute()) + run( - args=("podman", "cp", str(src.absolute()), f"{CONTAINER_NAME}:{dest}") + args=("podman", "cp", src_str, dest_str) ) @@ -271,14 +279,24 @@ def copy_sdbus_sources() -> None: def compile_sdbus() -> None: podman_exec( "python3", "setup.py", "build", "bdist_wheel", + "--py-limited-api", "cp39", cwd=SDBUS_SRC_DIR, env={ "PYTHON_SDBUS_USE_STATIC_LINK": "1", "PYTHON_SDBUS_USE_LIMITED_API": "1", + "CFLAGS": " ".join(BASIC_CFLAGS), }, ) +def copy_dist() -> None: + podman_cp( + SDBUS_SRC_DIR / "dist", + BUILD_DIR / f"{CONTAINER_ARCH}-dist", + to_contatiner=False, + ) + + STAGES: dict[str, Callable[[], None]] = { "podman_start": podman_start, "install_packages": install_packages, @@ -288,6 +306,7 @@ def compile_sdbus() -> None: "install_systemd_files": install_systemd_files, "copy_sdbus_sources": copy_sdbus_sources, "compile_sdbus": compile_sdbus, + "copy_dist": copy_dist, } @@ -306,6 +325,14 @@ def iter_stages() -> Iterator[tuple[str, Callable[[], None]]]: def main() -> None: + args_parser = ArgumentParser() + args_parser.add_argument("--arch") + args = args_parser.parse_args() + + if arch := args.arch: + global CONTAINER_ARCH + CONTAINER_ARCH = arch + BUILD_DIR.mkdir(parents=True, exist_ok=True) for stage_name, stage_func in iter_stages(): From bd9c5bdacf323afc677844a291aceb916d45c228 Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sat, 5 Apr 2025 20:41:06 +0100 Subject: [PATCH 153/188] Increase Python C limited API version to 3.9 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 1ecd2a1..c4cb434 100644 --- a/setup.py +++ b/setup.py @@ -82,7 +82,7 @@ def get_link_arguments() -> list[str]: use_limited_api = False if environ.get('PYTHON_SDBUS_USE_LIMITED_API'): - c_macros.append(('Py_LIMITED_API', '0x03070000')) + c_macros.append(('Py_LIMITED_API', '0x03090000')) use_limited_api = True From 7c40f7ce5eaff8d54dc24f881d0503b9624b7db8 Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sat, 5 Apr 2025 20:41:47 +0100 Subject: [PATCH 154/188] Copy auditwheel wrapper from python-lxns It adds ability to override architecture for auditwheel. --- wheel-build/audit_wheel_wrapper.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 wheel-build/audit_wheel_wrapper.py diff --git a/wheel-build/audit_wheel_wrapper.py b/wheel-build/audit_wheel_wrapper.py new file mode 100644 index 0000000..ec29ee7 --- /dev/null +++ b/wheel-build/audit_wheel_wrapper.py @@ -0,0 +1,27 @@ +# SPDX-License-Identifier: MPL-2.0 +# SPDX-FileCopyrightText: 2024 igo95862 +from __future__ import annotations + +from argparse import ArgumentParser +from unittest.mock import patch + +from auditwheel.main import main as auditwheel_main # type: ignore + + +def main(arch: str, wrapped_args: list[str]) -> None: + with patch("sys.argv", [""] + wrapped_args), patch( + "platform.machine", return_value=arch + ): + auditwheel_main() + + +if __name__ == "__main__": + arg_parse = ArgumentParser() + arg_parse.add_argument( + "--arch", + choices=("x86_64", "i686", "aarch64", "armv7l"), + default="x86_64", + ) + arg_parse.add_argument("wrapped_args", nargs="*") + + main(**vars(arg_parse.parse_args())) From 15cc3788cead2f0fcc8b02a9605ecc2ae5c24b27 Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sat, 5 Apr 2025 20:44:47 +0100 Subject: [PATCH 155/188] wheel-build: Remove old wheel build script --- meson.build | 1 - wheel-build/build_container_archive.py | 140 ------------ wheel-build/meson.build | 36 --- wheel-build/run_inside_container.py | 302 ------------------------- wheel-build/run_podman.py | 91 -------- 5 files changed, 570 deletions(-) delete mode 100755 wheel-build/build_container_archive.py delete mode 100644 wheel-build/meson.build delete mode 100755 wheel-build/run_inside_container.py delete mode 100755 wheel-build/run_podman.py diff --git a/meson.build b/meson.build index 7590d6b..b1ccea6 100644 --- a/meson.build +++ b/meson.build @@ -43,4 +43,3 @@ else endif subdir('src') -subdir('wheel-build') diff --git a/wheel-build/build_container_archive.py b/wheel-build/build_container_archive.py deleted file mode 100755 index 90b85ef..0000000 --- a/wheel-build/build_container_archive.py +++ /dev/null @@ -1,140 +0,0 @@ -#!/usr/bin/python3 -# SPDX-License-Identifier: LGPL-2.1-or-later - -# Copyright (C) 2020, 2021 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 argparse import ArgumentParser -from pathlib import Path -from shutil import copy -from subprocess import PIPE, run - -SYSTEMD_VERSION = '249.17' -UTIL_LINUX_VERSION = '2.37' -NINJA_VERSION = '1.10.2' -LIBCAP_VERSION = '2.69' - - -def create_archive(build_root: Path, output_file: Path) -> None: - run( - [ - 'tar', '--create', - '--file', str(output_file.absolute()), - '.', - ], - cwd=build_root.resolve(), - check=True, - ) - - -def download_source(target: Path, url: str) -> None: - run( - [ - 'curl', '--fail', '--location', - url, '--output', str(target) - ], - check=True, - ) - - -def download_systemd_source(build_dir: Path) -> None: - systemd_download_url = ( - "https://github.com/systemd/systemd-stable/" - f"archive/refs/tags/v{SYSTEMD_VERSION}.tar.gz" - ) - systemd_download_file = build_dir / "systemd.tar.gz" - - util_linux_src_url = ( - "https://mirrors.edge.kernel.org/pub/linux/utils/util-linux/" - f"v{UTIL_LINUX_VERSION}/util-linux-{UTIL_LINUX_VERSION}.tar.xz" - ) - util_linux_download_file = build_dir / "util_linux.tar.xz" - - ninja_src_url = ( - "https://github.com/ninja-build/ninja/" - f"archive/refs/tags/v{NINJA_VERSION}.tar.gz" - ) - ninja_download_file = build_dir / "ninja.tar.gz" - - libcap_src_url = ( - "https://kernel.org/pub/linux/libs/security/" - f"linux-privs/libcap2/libcap-{LIBCAP_VERSION}.tar.xz" - ) - libcap_download_file = build_dir / 'libcap.tar.xz' - - download_source(systemd_download_file, systemd_download_url) - download_source(util_linux_download_file, util_linux_src_url) - download_source(ninja_download_file, ninja_src_url) - download_source(libcap_download_file, libcap_src_url) - - -def copy_git_ls_files(source_root: Path, build_root: Path) -> None: - git_ls = run( - ['git', 'ls-files'], - stdout=PIPE, - cwd=source_root.resolve(), - text=True, - check=True, - ) - - for file_relative_source_str in git_ls.stdout.splitlines(): - orig_file_path = source_root / file_relative_source_str - copy_file_path = build_root / "python-sdbus" / file_relative_source_str - if not orig_file_path.exists(): - raise ValueError('Path does not exist', orig_file_path) - - if orig_file_path.is_dir(): - continue - else: - copy_file_path.parent.mkdir(parents=True, exist_ok=True) - copy(orig_file_path, copy_file_path.parent) - - -def main() -> None: - parser = ArgumentParser() - parser.add_argument( - '--build-dir', - type=Path, - required=True, - ) - parser.add_argument( - '--output-file', - type=Path, - required=True, - ) - parser.add_argument( - '--source-root', - type=Path, - required=True, - ) - args = parser.parse_args() - - build_dir = args.build_dir - output_file = args.output_file - source_root = args.source_root - - copy_git_ls_files(source_root, build_dir) - download_systemd_source(build_dir) - - create_archive(build_dir, output_file) - - -if __name__ == '__main__': - main() diff --git a/wheel-build/meson.build b/wheel-build/meson.build deleted file mode 100644 index 07bc568..0000000 --- a/wheel-build/meson.build +++ /dev/null @@ -1,36 +0,0 @@ -archive_builder = find_program('./build_container_archive.py') -podman_runner = find_program('./run_podman.py') -container_script = files('run_inside_container.py') - -build_container_archive = custom_target( - 'container_archive.tar', - build_by_default : false, - output : 'container_archive.tar', - input : container_script, # Force archive rebuilt on script changes - command : [ - archive_builder, - '--build-dir', '@PRIVATE_DIR@', - '--output-file', '@OUTPUT@', - '--source-root', '@SOURCE_ROOT@', - ], -) - -run_podman_x86_64 = run_target( - 'run_podman_x86_64', - command : [ - podman_runner, - '--archive', build_container_archive, - '--source-root', '@SOURCE_ROOT@', - '--arch', 'x86_64', - ], -) - -run_podman_aarch64 = run_target( - 'run_podman_aarch64', - command : [ - podman_runner, - '--archive', build_container_archive, - '--source-root', '@SOURCE_ROOT@', - '--arch', 'aarch64', - ], -) diff --git a/wheel-build/run_inside_container.py b/wheel-build/run_inside_container.py deleted file mode 100755 index 100a8fc..0000000 --- a/wheel-build/run_inside_container.py +++ /dev/null @@ -1,302 +0,0 @@ -#!/opt/python/cp39-cp39/bin/python3 -# SPDX-License-Identifier: LGPL-2.1-or-later - -# Copyright (C) 2020, 2021 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 os import environ, execl -from pathlib import Path -from shutil import copy -from subprocess import PIPE, CalledProcessError, run - -yum_packages: list[str] = [ - 'gettext-autopoint', 'gperf', -] - -# env -# export PATH="/opt/python/cp39-cp39/bin:${PATH}" - -# util-linux -# AL_OPTS="-I/usr/share/aclocal/" ./autogen.sh -# ./configure -# --prefix '/usr/local' --libdir '/usr/local/lib64' -# --enable-symvers -# --with-pkgconfigdir '/usr/share/pkgconfig/' - -# Ninja -# ./configure.py --boostrap -# cp ./ninja /usr/local/bin - -# systemd -# export PKG_CONFIG_PATH="/usr/local/lib64/pkgconfig" -# meson setup build -Dstatic-libsystemd=pic - -# PYTHON_SDBUS_USE_STATIC_LINK=1 - -ROOT_DIR = Path('/root') -NPROC = '4' -PYTHON_VERSIONS = ['cp39-cp39'] - -BASIC_C_FLAGS: list[str] = [ - '-O2', '-fno-plt', '-D_FORTIFY_SOURCE=2', - '-fstack-clash-protection', -] - -SYSTEMD_OPTIONS: list[str] = [ - "static-libsystemd=pic", - "tests=false", - "coredump=false", - "dbus=false", - "efi=false", - "elfutils=false", - "hostnamed=false", - "homed=false", - "importd=false", - "initrd=false", - "kernel-install=false", - "logind=false", - "machined=false", - "man=false", - "networkd=false", - "portabled=false", - "repart=false", - "sysext=false", - "sysusers=false", - "timedated=false", - "timesyncd=false", - "tmpfiles=false", - "oomd=false", - "hibernate=false", - "nss-systemd=false", - "nss-resolve=false", -] - - -NINJA_ARCHIVE = ROOT_DIR / "ninja.tar.gz" -NINJA_SRC_PATH = ROOT_DIR / 'src_ninja' - -UTIL_LINUX_ARCHIVE = ROOT_DIR / "util_linux.tar.xz" -UTIL_LINUX_SRC_PATH = ROOT_DIR / 'src_util_linux' - -LIBCAP_ARCHIVE = ROOT_DIR / "libcap.tar.xz" -LIBCAP_SRC_PATH = ROOT_DIR / 'src_libcap' - -SYSTEMD_ARCHIVE = ROOT_DIR / "systemd.tar.gz" -SYSTEMD_SRC_PATH = ROOT_DIR / 'src_systemd' - - -def unpack_archives() -> None: - for archive, to in ( - (NINJA_ARCHIVE, NINJA_SRC_PATH), - (UTIL_LINUX_ARCHIVE, UTIL_LINUX_SRC_PATH), - (LIBCAP_ARCHIVE, LIBCAP_SRC_PATH), - (SYSTEMD_ARCHIVE, SYSTEMD_SRC_PATH), - ): - to.mkdir(exist_ok=True) - run( - [ - "tar", "--verbose", - "--directory", str(to), - "--strip-components=1", - "--extract", "--file", str(archive) - ], - check=True, - ) - - -def setup_env() -> None: - python_bin_paths = (f"/opt/python/{x}/bin" for x in PYTHON_VERSIONS) - - environ['PATH'] = f"{':'.join(python_bin_paths)}:{environ['PATH']}" - environ['PYTHON_SDBUS_USE_STATIC_LINK'] = '1' - - audit_wheel_arch = environ['AUDITWHEEL_ARCH'] - - if audit_wheel_arch == 'x86_64': - BASIC_C_FLAGS.extend( - ( - '-march=x86-64', '-mtune=generic', - '-fcf-protection', # cf-protection only available on x86_64 - ) - ) - elif audit_wheel_arch == 'aarch64': - BASIC_C_FLAGS.extend(('-march=armv8-a', '-mtune=generic')) - else: - print('PYTHON-SDBUS: Unknown arch') - - new_cflags = ' '.join(BASIC_C_FLAGS) - environ['CFLAGS'] = new_cflags - environ['CXXFLAGS'] = new_cflags - - nproc = run( - ['nproc'], - stdout=PIPE, - text=True, - check=True, - ) - - global NPROC - NPROC = nproc.stdout.splitlines()[0] - - -def install_packages() -> None: - run( - ['yum', 'install', '--assumeyes'] + yum_packages, - check=True, - ) - - -def install_ninja() -> None: - - ninja_boot_strap_path = NINJA_SRC_PATH / 'configure.py' - - run( - [ninja_boot_strap_path, '--bootstrap'], - cwd=NINJA_SRC_PATH, - check=True, - ) - - copy(NINJA_SRC_PATH / 'ninja', '/usr/local/bin') - - -def install_meson() -> None: - run( - ['pip3', 'install', 'meson==1.4.0', 'Jinja2==3.1.1'], - check=True, - ) - - -def install_util_linux() -> None: - run( - [UTIL_LINUX_SRC_PATH / 'autogen.sh'], - cwd=UTIL_LINUX_SRC_PATH, - env={'AL_OPTS': '-I/usr/share/aclocal/', **environ}, - check=True, - ) - - run( - [ - UTIL_LINUX_SRC_PATH / 'configure', - '--prefix', '/usr/local', - '--libdir', '/usr/local/lib64', - '--enable-symvers', - ], - cwd=UTIL_LINUX_SRC_PATH, - check=True, - ) - - run( - ['make', '--jobs', NPROC, 'install'], - cwd=UTIL_LINUX_SRC_PATH, - check=True, - ) - - -def install_libcap() -> None: - run( - ['make', '--jobs', NPROC, 'install'], - cwd=LIBCAP_SRC_PATH, - check=True, - ) - - -def install_systemd() -> None: - systemd_build_path = ROOT_DIR / 'build_systemd' - - run( - ['meson', 'setup', - systemd_build_path, SYSTEMD_SRC_PATH, - '--buildtype', 'plain', - '-Db_lto=true', '-Db_pie=true', - *(f"-D{o}" for o in SYSTEMD_OPTIONS) - ], - env={**environ, 'PKG_CONFIG_PATH': '/usr/local/lib64/pkgconfig'}, - check=True, - ) - - run( - ['ninja', 'install'], - cwd=systemd_build_path, - check=True, - ) - - -def compile_extension() -> None: - python_sdbus_src_path = ROOT_DIR / 'python-sdbus' - setup_py_path = python_sdbus_src_path / 'setup.py' - build_dir_path = python_sdbus_src_path / 'build' - dist_dir_path = python_sdbus_src_path / 'dist' - repaired_wheels_path = ROOT_DIR / 'wheels' - - run( - [ - 'python3.8', setup_py_path, - 'build', 'bdist_wheel', - '--py-limited-api', 'cp37', - ], - cwd=python_sdbus_src_path, - check=True, - env={**environ, 'PYTHON_SDBUS_USE_LIMITED_API': '1'}, - ) - - run( - ['rm', '--recursive', build_dir_path], - cwd=python_sdbus_src_path, - check=True, - ) - - # Repair wheels - for wheel in dist_dir_path.iterdir(): - run( - [ - 'auditwheel', 'repair', - '--plat', environ['AUDITWHEEL_PLAT'], - '--strip', - '--wheel-dir', repaired_wheels_path, - wheel, - ], - check=True, - ) - - -def drop_to_shell() -> None: - execl('/bin/sh', '/bin/sh') - - -def main() -> None: - unpack_archives() - setup_env() - install_packages() - - install_ninja() - install_meson() - - install_util_linux() - install_libcap() - install_systemd() - - compile_extension() - - -if __name__ == '__main__': - try: - main() - except CalledProcessError: - drop_to_shell() diff --git a/wheel-build/run_podman.py b/wheel-build/run_podman.py deleted file mode 100755 index c4e8285..0000000 --- a/wheel-build/run_podman.py +++ /dev/null @@ -1,91 +0,0 @@ -#!/usr/bin/python3 -# SPDX-License-Identifier: LGPL-2.1-or-later - -# Copyright (C) 2020, 2021 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 argparse import ArgumentParser -from pathlib import Path -from shutil import copy -from subprocess import run -from tempfile import TemporaryDirectory - -MANYLINUX_VERSION = 'manylinux2014' - - -def run_podman( - archive: Path, - source_root: Path, - arch: str,) -> None: - wheels_root = source_root / 'dist' - - with TemporaryDirectory() as tmpdir: - run( - ['tar', '--extract', - '--directory', tmpdir, - '--file', str(archive)], - check=True, - ) - run( - ['podman', 'run', - '--arch', arch, - '--tty', '--interactive', '--rm', - '--volume', '.:/root', - f"quay.io/pypa/{MANYLINUX_VERSION}_{arch}", - '/root/python-sdbus/wheel-build/run_inside_container.py', - ], - cwd=tmpdir, - check=True, - ) - - wheels_root.mkdir(exist_ok=True) - for wheel in (Path(tmpdir) / 'wheels').iterdir(): - copy(wheel, wheels_root) - - -def main() -> None: - parser = ArgumentParser() - parser.add_argument( - '--archive', - type=Path, - required=True, - ) - parser.add_argument( - '--source-root', - type=Path, - required=True, - ) - parser.add_argument( - '--arch', - type=str, - choices=['x86_64', 'aarch64'], - default='x86_64', - ) - - args = parser.parse_args() - run_podman( - args.archive, - args.source_root, - args.arch, - ) - - -if __name__ == '__main__': - main() From 8564580d0df38b1659c676135310b82eb93d2ac4 Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sun, 6 Apr 2025 18:54:20 +0100 Subject: [PATCH 156/188] Add sdbus.default_bus.set_context_default_bus Sets the context-local default bus. The context bus only used when explicitly set to avoid bus initialization spam. Only having thread-local buses seemed very limiting. --- docs/general.rst | 20 ++++++++++++------- src/sdbus/__init__.py | 2 ++ src/sdbus/default_bus.py | 42 +++++++++++++++++++++++++++++++++++----- 3 files changed, 52 insertions(+), 12 deletions(-) diff --git a/docs/general.rst b/docs/general.rst index a9d9c4d..1a61eec 100644 --- a/docs/general.rst +++ b/docs/general.rst @@ -130,7 +130,7 @@ See API documentation for a particular decorator. Default bus -++++++++++++++++++++++++++ ++++++++++++ Most object methods that take a bus as a parameter will use a thread-local default bus connection if a bus object @@ -139,17 +139,23 @@ is not explicitly passed. Session bus is default bus when running as a user and system bus otherwise. -:py:func:`request_default_bus_name_async` can be used to acquire -a service name on default bus. +The :py:func:`request_default_bus_name_async ` +and :py:func:`request_default_bus_name ` +can be used to acquire a service name on the default bus. Use :py:func:`sd_bus_open_user` and :py:func:`sd_bus_open_system` to acquire a specific bus connection. -Set the default connection to a new default with :py:func:`set_default_bus`. -This should be done before any object that take bus as an init argument are created. +The :py:func:`set_default_bus ` can be used to set the new +thread-local bus. This should be done before any objects that take bus as +an init argument are created. If no bus has been set the new bus will +be initialized and set as thread-local default. -In the future there will be a better way to create and acquire -new bus connections. +The bus can also be set as default for the current context using +:py:func:`set_context_default_bus `. +The context refers to the standard library's ``contextvars`` module context variables +frequently used in asyncio frameworks. Context-local default bus has higher priority over +thread-local default bus. Glossary +++++++++++++++++++++ diff --git a/src/sdbus/__init__.py b/src/sdbus/__init__.py index e4529ed..0e0f7b8 100644 --- a/src/sdbus/__init__.py +++ b/src/sdbus/__init__.py @@ -74,6 +74,7 @@ get_default_bus, request_default_bus_name, request_default_bus_name_async, + set_context_default_bus, set_default_bus, ) from .sd_bus_internals import ( @@ -145,6 +146,7 @@ "get_default_bus", "request_default_bus_name", "request_default_bus_name_async", + "set_context_default_bus", "set_default_bus", 'DbusDeprecatedFlag', diff --git a/src/sdbus/default_bus.py b/src/sdbus/default_bus.py index fa9a89e..dfae614 100644 --- a/src/sdbus/default_bus.py +++ b/src/sdbus/default_bus.py @@ -20,6 +20,7 @@ from __future__ import annotations import threading +from contextvars import ContextVar, Token from logging import getLogger from typing import TYPE_CHECKING @@ -43,6 +44,7 @@ class DefaultBusTLStorage(threading.local): bus_tls = DefaultBusTLStorage() +bus_contextvar: ContextVar[SdBus] = ContextVar("DEFAULT_BUS") def _get_defaul_bus_tls() -> Optional[SdBus]: @@ -54,10 +56,20 @@ def _set_default_bus_tls(new_bus: Optional[SdBus]) -> None: def get_default_bus() -> SdBus: - """Get default thread-local bus.""" - current_bus = _get_defaul_bus_tls() - if current_bus is not None: - return current_bus + """Get default bus. + + Returns context-local default bus if set or + thread-local otherwise. + + If no default bus is set initializes a new bus using + :py:func:`sdbus.sd_bus_open` and sets it as a thread-local + default bus. + """ + if (context_bus := bus_contextvar.get(None)) is not None: + return context_bus + + if (tls_bus := _get_defaul_bus_tls()) is not None: + return tls_bus else: new_bus = sd_bus_open() logger.info( @@ -69,7 +81,7 @@ def get_default_bus() -> SdBus: def set_default_bus(new_default: SdBus) -> None: - """Set default thread-local bus. + """Set thread-local default bus. Should be called before creating any objects that will use default bus. @@ -80,6 +92,25 @@ def set_default_bus(new_default: SdBus) -> None: _set_default_bus_tls(new_default) +def set_context_default_bus(new_default: SdBus) -> Token[SdBus]: + """Set context-local default bus. + + Should be called before creating any objects that will use + default bus. + + Default bus can be replaced but the change will only affect + newly created objects. + + Context-local default bus has higher priority over thread-local one + but has to be explicitly set. + + :returns: + Token that can be used to reset context bus back. + See ``contextvars`` documentation for details. + """ + return bus_contextvar.set(new_default) + + def _prepare_request_name_flags( allow_replacement: bool, replace_existing: bool, @@ -167,6 +198,7 @@ def request_default_bus_name( __all__ = ( "get_default_bus", "set_default_bus", + "set_context_default_bus", "request_default_bus_name_async", "request_default_bus_name", ) From 95a8544404fa203a4be78366a7a21c25beb29e5a Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sun, 6 Apr 2025 18:59:40 +0100 Subject: [PATCH 157/188] Remove CodeQL workflow Not sure if it even did anything. --- .github/workflows/codeql.yml | 63 ------------------------------------ 1 file changed, 63 deletions(-) delete mode 100644 .github/workflows/codeql.yml diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml deleted file mode 100644 index 8a67e5c..0000000 --- a/.github/workflows/codeql.yml +++ /dev/null @@ -1,63 +0,0 @@ ---- -name: "CodeQL" - -on: - workflow_dispatch: - push: - branches: [ "master" ] - pull_request: - # The branches below must be a subset of the branches above - branches: [ "master" ] - schedule: - - cron: '43 21 * * 3' - -jobs: - analyze: - name: Analyze - runs-on: ubuntu-latest - permissions: - actions: read - contents: read - security-events: write - - strategy: - fail-fast: false - matrix: - language: [ 'python' ] - # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] - # Use only 'java' to analyze code written in Java, Kotlin or both - # Use only 'javascript' to analyze code written in JavaScript, TypeScript or both - # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support - - steps: - - name: Checkout repository - uses: actions/checkout@v3 - - # Initializes the CodeQL tools for scanning. - - name: Initialize CodeQL - uses: github/codeql-action/init@v2 - with: - languages: ${{ matrix.language }} - # If you wish to specify custom queries, you can do so here or in a config file. - # By default, queries listed here will override any specified in a config file. - # Prefix the list here with "+" to use these queries and those in the config file. - - # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java). - # If this step fails, then you should remove it and run the build manually (see below) - - name: Autobuild - uses: github/codeql-action/autobuild@v2 - - # ℹ️ Command-line programs to run using the OS shell. - # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun - - # If the Autobuild fails above, remove it and uncomment the following three lines. - # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. - - # - run: | - # echo "Run, Build Application using script" - # ./location_of_script_within_repo/buildscript.sh - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 - with: - category: "/language:${{matrix.language}}" From 7f731041ecffdd3d41f3709388110b5f1cd949c0 Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sun, 6 Apr 2025 19:01:58 +0100 Subject: [PATCH 158/188] Fix typos in src/sdbus/default_bus.py --- src/sdbus/default_bus.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sdbus/default_bus.py b/src/sdbus/default_bus.py index dfae614..ad10068 100644 --- a/src/sdbus/default_bus.py +++ b/src/sdbus/default_bus.py @@ -131,7 +131,7 @@ async def request_default_bus_name_async( replace_existing: bool = False, queue: bool = False, ) -> None: - r"""Asyncronously acquire a name on the default bus. + r"""Asynchronously acquire a name on the default bus. :param new_name: Name to acquire. @@ -167,7 +167,7 @@ def request_default_bus_name( ) -> None: r"""Acquire a name on the default bus. - Blocks until a reply is recieved from D-Bus daemon. + Blocks until a reply is received from D-Bus daemon. :param new_name: Name to acquire. From 737d5d0aa4ea08c6e011e6b39c8bf58ea70fee05 Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sun, 6 Apr 2025 19:14:44 +0100 Subject: [PATCH 159/188] Update binary package requirements in README.md Python version raised to 3.9. Glibc raised to 2.28. Added `armv7l` to list of supported architectures. --- README.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 08bc9c5..20dc538 100644 --- a/README.md +++ b/README.md @@ -50,13 +50,13 @@ please open a new issue. ### Binary package from PyPI -* Python 3.8 or higher. (3.7 might work but is not supported) -* `x86_64` or `aarch64` architecture. -* glibc 2.17 or higher. (released in 2014) -* pip 19.3 or higher. +* Python 3.9 or higher. +* `x86_64`, `aarch64` or `armv7l` architecture. +* glibc 2.28 or higher. (Debian 10+, Ubuntu 18.10+, CentOS/RHEL 8+) +* pip 20.3 or higher. -Starting with version `0.8rc2` the libsystemd is statically -linked and is not required. +`libsystemd` is statically linked and is not required to be installed +on the system. Pass `--only-binary ':all:'` to pip to ensure that it installs binary package. @@ -67,7 +67,7 @@ platforms. ### Source package or compiling from source -* Python 3.8 or higher. +* Python 3.9 or higher. * Python headers. (`python3-dev` package on ubuntu) * GCC. * libsystemd or libelogind From 52ffa6a7c504be511dafaef8c2997bf3f3aa13bd Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sun, 6 Apr 2025 19:21:10 +0100 Subject: [PATCH 160/188] test: Add default bus tests Check that context vars work as expected. --- test/test_default_bus.py | 60 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 test/test_default_bus.py diff --git a/test/test_default_bus.py b/test/test_default_bus.py new file mode 100644 index 0000000..1e08d34 --- /dev/null +++ b/test/test_default_bus.py @@ -0,0 +1,60 @@ +# 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 contextvars import copy_context +from unittest import main + +from sdbus.unittest import IsolatedDbusTestCase + +from sdbus import get_default_bus, sd_bus_open_user, set_context_default_bus + + +def return_bus_id() -> int: + return id(get_default_bus()) + + +def set_context_and_return_id() -> int: + set_context_default_bus(sd_bus_open_user()) + return id(get_default_bus()) + + +class TestDefaultBus(IsolatedDbusTestCase): + def test_context_bus(self) -> None: + bus_id = id(get_default_bus()) + + self.assertEqual( + bus_id, + copy_context().run(return_bus_id), + ) + + self.assertNotEqual( + bus_id, + copy_context().run(set_context_and_return_id), + ) + + self.assertEqual( + bus_id, + id(get_default_bus()), + ) + + +if __name__ == "__main__": + main() From d87ff13a7ed00e86467b162f8369559f22e50b8b Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sun, 6 Apr 2025 20:25:25 +0100 Subject: [PATCH 161/188] Version 0.14.0 --- CHANGELOG.md | 58 ++++++++++++++++++++++++++++++++++++++++++++++++++++ setup.py | 2 +- 2 files changed, 59 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e494018..2a5a806 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,61 @@ +## 0.14.0 + +### Minimum requirements raised + +* Python 3.9 or higher. + +For binary PyPI wheel: + +* glibc 2.28 or higher. (Debian 10+, Ubuntu 18.10+, CentOS/RHEL 8+) +* pip 20.3 or higher. +* Added 32 bit ARM (`armv7l`) architecture wheel. + +### Default bus changes + +Previously the default bus always used the context-local variables +to store the reference to the current default bus. As it turned out +the context tends to be changed a lot which resulted in new buses being +opened multiple times. (reported by @wes8ty) + +To avoid this the default bus was changed to be thread-local. +`set_default_bus` will now set the thread-local default bus. +A new function `set_context_default_bus` was added to set the context-local +bus. The `get_default_bus` will return the context-local bus if set or +thread-local otherwise. If no default bus has been set a new thread-local +bus will be initialized and set. + +### Code generator + +* Code generator will now add manual D-Bus member name override where + automatic snake_case to CamelCase does not result in the original member name. + This applies to when member renaming options were used. (reported by @nicomuns) +* Generated code will now use Python 3.9 built-in collections type hints. + (`typing.List[str]` -> `list[str]`) +* Fixed blocking generated code adding unexpected `result_args_names` keyword. + (reported by @christophehenry) + +### Features + +* All `sdbus.utils.parse` functions can now accept the blocking interfaces. + (requested by @christophehenry) +* Added boolean `use_interface_subsets` option to `sdbus.utils.parse` functions. + When enabled the subset of interfaces will be considered a valid match. + (requested by @christophehenry) + +### Fixes + +* Fixed exceptions mapped by `map_exception_to_dbus_error` not being translated + from Python to D-Bus errors. This means the Python built-in exceptions will + now be properly returned as D-Bus errors when raised in exported object callback. + The built-in exceptions translating as added back in version 0.10.0 but probably + never worked correctly. (reported by @arkq) +* Fixed not being able to export interfaces with no implemented D-Bus members. + This also means `export_to_dbus` will only access D-Bus related attributes + avoiding triggering unrelated `@property` methods. +* Renamed certain internal classes from `Binded` to `Bound` and + from `DbusSomething` to `DbusMember`. (reported by @souliane, + implemented by @dragomirecky) + ## 0.13.0 ### Code generator improvements diff --git a/setup.py b/setup.py index c4cb434..a4fed7c 100644 --- a/setup.py +++ b/setup.py @@ -96,7 +96,7 @@ def get_link_arguments() -> list[str]: 'Based on sd-bus from libsystemd.'), long_description=long_description, long_description_content_type='text/markdown', - version='0.13.0', + version='0.14.0', url='https://github.com/igo95862/python-sdbus', author='igo95862', author_email='igo95862@yandex.ru', From 3392530cc32c340450f36d114ade574a5454b270 Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sun, 6 Apr 2025 21:25:17 +0100 Subject: [PATCH 162/188] Remove CodeQL badge from README.md --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 20dc538..e3ccb24 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,3 @@ -[![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) # Modern Python library for D-Bus From 5f7ae7a78ac26625928c467308b9376a12e79b6f Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sun, 6 Apr 2025 23:09:35 +0100 Subject: [PATCH 163/188] Add PyPI badge to the README.md Shows most recent version on PyPI and links to the sdbus PyPI page. --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index e3ccb24..da37713 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ [![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 From 7de5e6b48f309dc7c09e497c7c6389d8af311f83 Mon Sep 17 00:00:00 2001 From: Arkadiusz Bokowy Date: Wed, 9 Apr 2025 21:34:49 +0200 Subject: [PATCH 164/188] Fix InterfacesRemoved signal emitted when export handle is stopped --- src/sdbus/dbus_proxy_async_object_manager.py | 2 +- test/test_object_manager.py | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) 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/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 From 4074f9510bd2c9e46679f80593e54dafb86ad536 Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sun, 13 Apr 2025 00:05:37 +0100 Subject: [PATCH 165/188] Use METH_O method type for SdBus.call(_async) METH_O methods only take a single argument. It does not look like it constructs any tuples to make the call so it should be as fast as METH_FASTCALL without being only available for unlimited API in Python 3.9. Benchmarks did not reveal any significant impact. --- src/sdbus/sd_bus_internals_bus.c | 35 ++++++++------------------------ 1 file changed, 8 insertions(+), 27 deletions(-) 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.")}, From 9e2950f3af6e91b714f895d96c166f058dbc905b Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sun, 13 Apr 2025 00:32:19 +0100 Subject: [PATCH 166/188] test: Add blocking ping benchmark Calls `Ping` D-Bus method on the daemon in sequence. --- test/benchmarks/bench_block_ping.py | 52 +++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 test/benchmarks/bench_block_ping.py 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() From 9c4c2fc9908a9663b31624d8b2a03b16acf30b53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20M=C3=A9lotte?= Date: Thu, 17 Apr 2025 14:14:22 +0200 Subject: [PATCH 167/188] Update URL to github repository MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The github URL has changed but it was not updated at least in some places, so update it now. Signed-off-by: Raphaël Mélotte --- docs/autodoc.rst | 2 +- setup.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) 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', From bda5dfaf09ccf89886dbc80b1956beed10159886 Mon Sep 17 00:00:00 2001 From: igo95862 Date: Mon, 21 Apr 2025 20:02:31 +0100 Subject: [PATCH 168/188] Remove old unused files --- .python-version | 2 -- tox.ini | 6 ------ 2 files changed, 8 deletions(-) delete mode 100644 .python-version delete mode 100644 tox.ini 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/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 - From eb8900ab97e4923faf965f3df2b6a61053820e4b Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sun, 11 May 2025 23:12:18 +0100 Subject: [PATCH 169/188] ci: Run CI weekly The lint tools are continuously updated which means new errors could be found. Check for new errors every week. --- .github/workflows/ubuntu_test.yml | 2 ++ 1 file changed, 2 insertions(+) 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: From 9adcb23e28d213f2171ecc0b780637a0815d5308 Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sat, 31 May 2025 22:17:59 +0100 Subject: [PATCH 170/188] Fix internal exception typing The `add_exception_mapping` should take the exception type as an argument not initialized exception. --- src/sdbus/dbus_exceptions.py | 2 +- src/sdbus/sd_bus_internals.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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/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 From b7b9c5c7a152c9dec7b5b9650aa7e44cb403d5c6 Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sat, 31 May 2025 22:46:02 +0100 Subject: [PATCH 171/188] Add workaround for internal override assignment typing Apparently there is confusion between `MethodType` and `FunctionType` in the older versions of mypy. Add an ignore comment to have it be compatible between mypy versions. --- src/sdbus/dbus_proxy_async_interface_base.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/sdbus/dbus_proxy_async_interface_base.py b/src/sdbus/dbus_proxy_async_interface_base.py index e0b6f5f..0d0dd58 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 From b853432e57afd4d7e282f33c733d9ebed9abf787 Mon Sep 17 00:00:00 2001 From: Arkadiusz Bokowy Date: Thu, 12 Jun 2025 16:49:23 +0200 Subject: [PATCH 172/188] Iterate interfaces in the MRO reversed order Iterating the MRO in the reversed order will allow to add interfaces to the given object in a well-defined way - always starting from the base class. Such ordering will be compatible with adding interfaces manually one by one. --- src/sdbus/dbus_proxy_async_interface_base.py | 2 +- src/sdbus/dbus_proxy_async_interfaces.py | 5 +++-- src/sdbus/dbus_proxy_sync_interface_base.py | 2 +- src/sdbus/dbus_proxy_sync_interfaces.py | 4 ++-- test/test_sdbus_async.py | 16 +++++++++++++++- test/test_sdbus_block.py | 16 +++++++++++++++- 6 files changed, 37 insertions(+), 8 deletions(-) diff --git a/src/sdbus/dbus_proxy_async_interface_base.py b/src/sdbus/dbus_proxy_async_interface_base.py index 0d0dd58..6fb927e 100644 --- a/src/sdbus/dbus_proxy_async_interface_base.py +++ b/src/sdbus/dbus_proxy_async_interface_base.py @@ -295,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_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/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() From ca858850f43b1d8e1a992398be2c8a0bc1937219 Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sun, 3 Aug 2025 22:52:15 +0100 Subject: [PATCH 173/188] Add blocking variant of the simple example Shows how to define the blocking interface and call methods or properties. --- examples/simple/client_blocking.py | 64 ++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 examples/simple/client_blocking.py diff --git a/examples/simple/client_blocking.py b/examples/simple/client_blocking.py new file mode 100644 index 0000000..453f13f --- /dev/null +++ b/examples/simple/client_blocking.py @@ -0,0 +1,64 @@ +# 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 sdbus import DbusInterfaceCommon, dbus_method, dbus_property + +# The interface has to be redefined using the blocking base class +# and decorators. + + +class ExampleInterfaceBlocking( + DbusInterfaceCommon, interface_name="org.example.interface" +): + @dbus_method( + input_signature="s", + result_signature="s", + ) + def upper(self, string: str) -> str: + return string.upper() + + @dbus_property( + property_signature="s", + ) + def hello_world(self) -> str: + return "Hello, World!" + + +def main() -> None: + # Create a new proxied object + example_object = ExampleInterfaceBlocking( + service_name="org.example.test", + object_path="/", + ) + + # Call upper + s = "test string" + s_after = example_object.upper(s) + + print("Initial string: ", s) + print("After call: ", s_after) + + # Get property + print("Remote property: ", example_object.hello_world) + + +if __name__ == "__main__": + main() From f62c8bf9b6a3d16b71526a78c73e57cd59145d7a Mon Sep 17 00:00:00 2001 From: igo95862 Date: Wed, 10 Sep 2025 21:17:10 +0100 Subject: [PATCH 174/188] Fix certain f-strings missing f prefix --- src/sdbus/dbus_proxy_async_interface_base.py | 2 +- src/sdbus/interface_generator.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sdbus/dbus_proxy_async_interface_base.py b/src/sdbus/dbus_proxy_async_interface_base.py index 6fb927e..f089413 100644 --- a/src/sdbus/dbus_proxy_async_interface_base.py +++ b/src/sdbus/dbus_proxy_async_interface_base.py @@ -385,7 +385,7 @@ def export_to_dbus( ) else: raise TypeError( - "Expected D-Bus element, got: {dbus_something!r}" + f"Expected D-Bus element, got: {dbus_something!r}" ) bus.add_interface(new_interface, object_path, interface_name) diff --git a/src/sdbus/interface_generator.py b/src/sdbus/interface_generator.py index 0c87b8b..27aca92 100644 --- a/src/sdbus/interface_generator.py +++ b/src/sdbus/interface_generator.py @@ -226,7 +226,7 @@ def typing_complete(cls, complete_sig: str) -> str: if len(array_completes) != 1: raise ValueError("Array does not have only " - "one complete type: {array_completes}") + f"one complete type: {array_completes}") array_single_complete = array_completes[0] From 54ffe5bba43812ae95c541452e52927c4b888f4b Mon Sep 17 00:00:00 2001 From: igo95862 Date: Tue, 9 Sep 2025 20:56:16 +0100 Subject: [PATCH 175/188] Use blocking function to integrate Python methods and sd-bus Current asyncio callbacks have an issue of being garbage collected. In order to prevent that add them to a set and use `add_done_callback` to drop refrence once task completes. Make `DbusLocalMethodAsync._dbus_reply_call` a blocking function as in the future the low level API will no longer have asyncio integration. Bug reported by @arkq. --- src/sdbus/dbus_common_elements.py | 11 ++++++++++ src/sdbus/dbus_proxy_async_interface_base.py | 4 ++++ src/sdbus/dbus_proxy_async_method.py | 21 ++++++++++++++++++-- src/sdbus/sd_bus_internals.py | 4 ++-- 4 files changed, 36 insertions(+), 4 deletions(-) diff --git a/src/sdbus/dbus_common_elements.py b/src/sdbus/dbus_common_elements.py index 703b9d0..4df138f 100644 --- a/src/sdbus/dbus_common_elements.py +++ b/src/sdbus/dbus_common_elements.py @@ -30,6 +30,7 @@ from .sd_bus_internals import is_interface_name_valid, is_member_name_valid if TYPE_CHECKING: + from asyncio import Task from collections.abc import Callable, Sequence from types import FunctionType from typing import Any, Optional @@ -330,6 +331,16 @@ def __init__(self) -> None: self.activated_interfaces: list[SdBusInterface] = [] self.serving_object_path: Optional[str] = None self.attached_bus: Optional[SdBus] = None + self._tasks: Optional[set[Task[None]]] = None + + @property + def tasks(self) -> set[Task[None]]: + tasks_set = self._tasks + if tasks_set is None: + tasks_set = set() + self._tasks = tasks_set + + return tasks_set class DbusClassMeta: diff --git a/src/sdbus/dbus_proxy_async_interface_base.py b/src/sdbus/dbus_proxy_async_interface_base.py index f089413..2872669 100644 --- a/src/sdbus/dbus_proxy_async_interface_base.py +++ b/src/sdbus/dbus_proxy_async_interface_base.py @@ -452,6 +452,7 @@ def new_proxy( class DbusExportHandle: def __init__(self, local_meta: DbusLocalObjectMeta): + self._tasks = local_meta.tasks self._dbus_slots: list[SdBusSlot] = [ i.slot for i in local_meta.activated_interfaces @@ -481,5 +482,8 @@ async def __aexit__( self.stop() def stop(self) -> None: + for task in self._tasks: + task.cancel("D-Bus export stopped") + for slot in self._dbus_slots: slot.close() diff --git a/src/sdbus/dbus_proxy_async_method.py b/src/sdbus/dbus_proxy_async_method.py index 49e304b..3ac3c2a 100644 --- a/src/sdbus/dbus_proxy_async_method.py +++ b/src/sdbus/dbus_proxy_async_method.py @@ -19,6 +19,7 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA from __future__ import annotations +from asyncio import get_running_loop from contextvars import ContextVar, copy_context from inspect import iscoroutinefunction from types import FunctionType @@ -27,6 +28,7 @@ from .dbus_common_elements import ( DbusBoundAsync, + DbusLocalObjectMeta, DbusMemberAsync, DbusMethodCommon, DbusMethodOverride, @@ -177,16 +179,31 @@ async def _dbus_reply_call_method( return await local_method(*request_message.parse_to_tuple()) - async def _dbus_reply_call( + def _dbus_reply_call( self, request_message: SdBusMessage ) -> None: local_object = self.local_object_ref() if local_object is None: raise RuntimeError("Local object no longer exists!") + local_meta = local_object._dbus + if not isinstance(local_meta, DbusLocalObjectMeta): + raise RuntimeError("D-Bus object is a remote proxy!") - call_context = copy_context() + loop = get_running_loop() + reply_task = loop.create_task( + self._dbus_reply_call_async(local_object, request_message) + ) + tasks_set = local_meta.tasks + tasks_set.add(reply_task) + reply_task.add_done_callback(tasks_set.discard) + async def _dbus_reply_call_async( + self, + local_object: DbusInterfaceBaseAsync, + request_message: SdBusMessage + ) -> None: + call_context = copy_context() try: reply_data = await call_context.run( self._dbus_reply_call_method, diff --git a/src/sdbus/sd_bus_internals.py b/src/sdbus/sd_bus_internals.py index 54099d4..6319c06 100644 --- a/src/sdbus/sd_bus_internals.py +++ b/src/sdbus/sd_bus_internals.py @@ -23,7 +23,7 @@ from typing import TYPE_CHECKING if TYPE_CHECKING: - from collections.abc import Callable, Coroutine, Sequence + from collections.abc import Callable, Sequence from typing import Any, Optional, Union DbusBasicTypes = Union[str, int, bytes, float, Any] @@ -63,7 +63,7 @@ def add_method( signature: str, input_args_names: Sequence[str], result_signature: str, result_args_names: Sequence[str], flags: int, - callback: Callable[[SdBusMessage], Coroutine[Any, Any, None]], / + callback: Callable[[SdBusMessage], None], / ) -> None: raise NotImplementedError(__STUB_ERROR) From f572c92a3353e85a58f31d7fb25113421baf35f3 Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sat, 13 Sep 2025 22:27:06 +0100 Subject: [PATCH 176/188] Add sdbus.utils.inspect.inspect_dbus_attached_bus function Returns the D-Bus bus used by the object. --- src/sdbus/utils/inspect.py | 26 ++++++++++++++++++++++++++ test/test_sdbus_utils.py | 23 ++++++++++++++++++++++- 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/src/sdbus/utils/inspect.py b/src/sdbus/utils/inspect.py index 92dce77..5306db7 100644 --- a/src/sdbus/utils/inspect.py +++ b/src/sdbus/utils/inspect.py @@ -110,6 +110,32 @@ def inspect_dbus_path( raise TypeError(f"Expected D-Bus object got {obj!r}") +def inspect_dbus_bus( + obj: Union[DbusInterfaceBase, DbusInterfaceBaseAsync] +) -> Optional[SdBus]: + """Return D-Bus bus used by the object. + + If called on D-Bus proxies or exported local D-Bus objects returns + bus object. + + If called on local D-Bus objects that had not been exported returns None. + + If called on an object that is unrelated to D-Bus raises ``TypeError``. + + :param obj: + Object to inspect. + :returns: + D-Bus bus object. + + *New in version 0.14.1.* + """ + if isinstance(obj, (DbusInterfaceBase, DbusInterfaceBaseAsync)): + return obj._dbus.attached_bus + else: + raise TypeError(f"Expected D-Bus object got {obj!r}") + + __all__ = ( + 'inspect_dbus_bus', "inspect_dbus_path", ) diff --git a/test/test_sdbus_utils.py b/test/test_sdbus_utils.py index 68b59d4..49b443c 100644 --- a/test/test_sdbus_utils.py +++ b/test/test_sdbus_utils.py @@ -22,7 +22,7 @@ from unittest import TestCase from sdbus.unittest import IsolatedDbusTestCase -from sdbus.utils.inspect import inspect_dbus_path +from sdbus.utils.inspect import inspect_dbus_bus, inspect_dbus_path from sdbus.utils.parse import parse_get_managed_objects from sdbus import ( @@ -258,3 +258,24 @@ def test_inspect_dbus_path_async_local(self) -> None: with self.assertRaisesRegex(LookupError, "is not attached to bus"): inspect_dbus_path(local_obj, new_bus) + + def test_inspect_attached_bus(self) -> None: + proxy = DbusInterfaceCommon("example.org", TEST_PATH) + + self.assertIs(inspect_dbus_bus(proxy), self.bus) + + with self.assertRaises(TypeError): + inspect_dbus_bus(object()) # type: ignore[arg-type] + + def test_inspect_attached_bus_async(self) -> None: + proxy = DbusInterfaceCommonAsync.new_proxy("example.org", TEST_PATH) + + self.assertIs(inspect_dbus_bus(proxy), self.bus) + + local_obj = FooBarAsync() + + self.assertIsNone(inspect_dbus_bus(local_obj)) + + local_obj.export_to_dbus("/") + + self.assertIs(inspect_dbus_bus(local_obj), self.bus) From 0eeaf3a902e7fb7d4dcf86e393939db2a7c8fbff Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sun, 14 Sep 2025 10:58:47 +0100 Subject: [PATCH 177/188] Fix Debian 11 backports URL It was moved to `archive.debian.org` domain. --- wheel-build/run_podman_full_build.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/wheel-build/run_podman_full_build.py b/wheel-build/run_podman_full_build.py index 8bae78e..2231b6d 100644 --- a/wheel-build/run_podman_full_build.py +++ b/wheel-build/run_podman_full_build.py @@ -157,7 +157,8 @@ def install_packages() -> None: podman_exec( "bash", "-c", - f"echo 'deb http://deb.debian.org/debian {DEBIAN_NAME}-backports main'" + "echo 'deb http://archive.debian.org/debian " + f"{DEBIAN_NAME}-backports main'" " > /etc/apt/sources.list.d/backports.list" ) podman_exec("apt-get", "update", env=deb_env) From 2dc88149118535c5e6759139a47ea8400cfca7c5 Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sun, 14 Sep 2025 11:32:13 +0100 Subject: [PATCH 178/188] wheel-build: Bump systemd minor version --- wheel-build/run_podman_full_build.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wheel-build/run_podman_full_build.py b/wheel-build/run_podman_full_build.py index 2231b6d..3f51f0a 100644 --- a/wheel-build/run_podman_full_build.py +++ b/wheel-build/run_podman_full_build.py @@ -60,7 +60,7 @@ SYSTEMD_REPO = "https://github.com/systemd/systemd-stable.git" # systemd 255 is last one before glibc 2.31 requirement -SYSTEMD_TAG = "v255.18" +SYSTEMD_TAG = "v255.22" SYSTEMD_SRC_DIR = Path("/root/systemd") SYSTEMD_BUILD_DIR = SYSTEMD_SRC_DIR / "build" SYSTEMD_COMPAT_PATCH_NAME = "systemd_no_gettid_no_getdents64.patch" From 17f50ffa1512cd52a74a22f634dd6d4e4457216b Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sun, 14 Sep 2025 13:31:12 +0100 Subject: [PATCH 179/188] wheel-build: Backport consistent interface order for libsystemd Requested by @arkq. --- wheel-build/consistent_interface_order.patch | 30 ++++++++++++++++++++ wheel-build/run_podman_full_build.py | 17 ++++++----- 2 files changed, 40 insertions(+), 7 deletions(-) create mode 100644 wheel-build/consistent_interface_order.patch diff --git a/wheel-build/consistent_interface_order.patch b/wheel-build/consistent_interface_order.patch new file mode 100644 index 0000000..e101fa1 --- /dev/null +++ b/wheel-build/consistent_interface_order.patch @@ -0,0 +1,30 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# SPDX-FileCopyrightText: 2025 Arkadiusz Bokowy +From 998aa62a21c69b34700f6cbbeb540beddffa9c89 Mon Sep 17 00:00:00 2001 +From: Arkadiusz Bokowy +Date: Thu, 12 Jun 2025 16:20:29 +0200 +Subject: [PATCH] sd-bus: Preserve interfaces addition order + +When adding a new interface to the object add it at the end of the list. +This way, when iterating over the list, e.g., during handling introspect +call, the order of returned interfaces will mach the order in which they +were added. +--- + src/libsystemd/sd-bus/bus-objects.c | 3 +++ + test/units/TEST-23-UNIT-FILE.oneshot-restart.sh | 2 +- + 2 files changed, 4 insertions(+), 1 deletion(-) + +diff --git a/src/libsystemd/sd-bus/bus-objects.c b/src/libsystemd/sd-bus/bus-objects.c +index 7309ad621a0a7..cc1ef226f0cdd 100644 +--- a/src/libsystemd/sd-bus/bus-objects.c ++++ b/src/libsystemd/sd-bus/bus-objects.c +@@ -1973,6 +1973,9 @@ static int add_object_vtable_internal( + } + } + ++ if (!existing) ++ existing = LIST_FIND_TAIL(vtables, n->vtables); ++ + s->node_vtable.node = n; + LIST_INSERT_AFTER(vtables, n->vtables, existing, &s->node_vtable); + bus->nodes_modified = true; diff --git a/wheel-build/run_podman_full_build.py b/wheel-build/run_podman_full_build.py index 3f51f0a..991706e 100644 --- a/wheel-build/run_podman_full_build.py +++ b/wheel-build/run_podman_full_build.py @@ -63,8 +63,10 @@ SYSTEMD_TAG = "v255.22" SYSTEMD_SRC_DIR = Path("/root/systemd") SYSTEMD_BUILD_DIR = SYSTEMD_SRC_DIR / "build" -SYSTEMD_COMPAT_PATCH_NAME = "systemd_no_gettid_no_getdents64.patch" -SYSTEMD_COMPAT_PATCH_FILE = WHEEL_BUILD_DIR / SYSTEMD_COMPAT_PATCH_NAME +SYSTEMD_COMPAT_PATCHES: list[str] = [ + "systemd_no_gettid_no_getdents64.patch", + "consistent_interface_order.patch", +] SYSTEMD_OPTIONS: list[str] = [ "static-libsystemd=pic", "tests=false", @@ -192,11 +194,12 @@ def clone_systemd() -> None: def apply_systemd_patch() -> None: - podman_cp(SYSTEMD_COMPAT_PATCH_FILE, SYSTEMD_SRC_DIR) - podman_exec( - "git", "apply", SYSTEMD_COMPAT_PATCH_NAME, - cwd=SYSTEMD_SRC_DIR, - ) + for patch_filename in SYSTEMD_COMPAT_PATCHES: + podman_cp(WHEEL_BUILD_DIR / patch_filename, SYSTEMD_SRC_DIR) + podman_exec( + "git", "apply", patch_filename, + cwd=SYSTEMD_SRC_DIR, + ) def build_systemd() -> None: From 71acc43e74053fc9b8bb70a54ce77b8bc795f70f Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sun, 14 Sep 2025 23:09:57 +0100 Subject: [PATCH 180/188] Version 0.14.1 --- CHANGELOG.md | 17 +++++++++++++++++ setup.py | 2 +- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a5a806..0e66c69 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,20 @@ +## 0.14.1 + +### Features + +* Added `sdbus.utils.inspect.inspect_dbus_bus` function. Returns a bus object + used by a proxy or exported local object. + +### Fixes + +* Fix object manager's `InterfacesRemoved` signal being emitted without having + interface names. (reported and fixed by @arkq) +* Fixed interface ordering for signals and methods that return interface information + like `InterfacesAdded` or `GetManagedObjects`. (reported and fixed by @arkq) +* Fixed exported methods callbacks sometimes getting garbage collected before + reply could be sent. (reported by @arkq) +* Fixed several documentation URLs linking outdated repository. (reported and fixed by @rmelotte) + ## 0.14.0 ### Minimum requirements raised diff --git a/setup.py b/setup.py index bee9877..08fb946 100644 --- a/setup.py +++ b/setup.py @@ -96,7 +96,7 @@ def get_link_arguments() -> list[str]: 'Based on sd-bus from libsystemd.'), long_description=long_description, long_description_content_type='text/markdown', - version='0.14.0', + version='0.14.1', url='https://github.com/python-sdbus/python-sdbus', author='igo95862', author_email='igo95862@yandex.ru', From 0b530df5ec06631989df91ecdc05b485fa2a9053 Mon Sep 17 00:00:00 2001 From: igo95862 Date: Tue, 16 Sep 2025 17:41:00 +0100 Subject: [PATCH 181/188] Rebuild all 0.14.1 wheels without binary strip Something got broken during strip process and non x86_64 wheels are broken. Reported by @adamshapiro0. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 08fb946..5b32c5b 100644 --- a/setup.py +++ b/setup.py @@ -96,7 +96,7 @@ def get_link_arguments() -> list[str]: 'Based on sd-bus from libsystemd.'), long_description=long_description, long_description_content_type='text/markdown', - version='0.14.1', + version='0.14.1.post0', url='https://github.com/python-sdbus/python-sdbus', author='igo95862', author_email='igo95862@yandex.ru', From 82c595e9916e08f946f77fa87fb841e44e049e59 Mon Sep 17 00:00:00 2001 From: igo95862 Date: Wed, 17 Sep 2025 18:39:30 +0100 Subject: [PATCH 182/188] github-ci: Run PyPI package test on AArch64 and Ubuntu 24.04 This will catch any issues with AArch64 packages like the one that recently occured with 0.14.1. --- .github/workflows/ubuntu_pypi_test.yml | 29 ++++++++++++++++++-------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ubuntu_pypi_test.yml b/.github/workflows/ubuntu_pypi_test.yml index 1698b15..495bc7f 100644 --- a/.github/workflows/ubuntu_pypi_test.yml +++ b/.github/workflows/ubuntu_pypi_test.yml @@ -1,31 +1,42 @@ --- -name: Install package from PyPI and run unit tests on Ubuntu 20.04 +name: Install package from PyPI and run unit tests on: workflow_dispatch: inputs: pypi_version: description: "Version specifier to install from PyPI" +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: false + jobs: run: - name: Install from PyPI and run unit tests - runs-on: ubuntu-22.04 + name: Install PyPI binary package and run unit tests + strategy: + matrix: + ubuntu_version: + - "ubuntu-22.04" + - "ubuntu-24.04" + - "ubuntu-22.04-arm" + - "ubuntu-24.04-arm" + runs-on: ${{ matrix.ubuntu_version }} steps: - name: Checkout uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 - name: Install dependencies run: | - sudo apt update - sudo apt install python3-setuptools \ - systemd dbus python3 python3-pip python3-jinja2 + sudo apt-get update + sudo apt-get install dbus python3 python3-pip python3-venv - name: Install package run: | - sudo pip3 install "sdbus ${SDBUS_VERSION}" + python3 -m venv venv + ./venv/bin/pip3 install --only-binary ':all:' "sdbus ${SDBUS_VERSION}" env: SDBUS_VERSION: ${{ inputs.pypi_version }} - name: List package run: | - pip3 list | grep sdbus + ./venv/bin/pip3 list | grep sdbus - name: Run unit tests run: | - python3 -m unittest + ./venv/bin/python3 -m unittest From 2e0ffa651e7956fb183e7f453f91da49979c386b Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sat, 15 Nov 2025 10:53:33 +0000 Subject: [PATCH 183/188] test: Increase timing tolerances Some slow CI runners might fail simply because of timings. --- test/test_sdbus_async.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_sdbus_async.py b/test/test_sdbus_async.py index 7e1d885..f5dff26 100644 --- a/test/test_sdbus_async.py +++ b/test/test_sdbus_async.py @@ -735,7 +735,7 @@ async def test_bus_timerfd(self) -> None: with self.assertRaises(DbusNoReplyError): await wait_for(test_object_connection.looong_method(), timeout=1) - self.assertAlmostEqual(loop.time() - start, 0.01, delta=0.01) + self.assertLess(loop.time() - start, 0.2) async def test_signal_queue_wildcard_match(self) -> None: test_object, test_object_connection = initialize_object() From e6039d8cd7ef86b2ab61ffc9bb2b889043a8df80 Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sun, 21 Dec 2025 19:17:04 +0000 Subject: [PATCH 184/188] benchmarks: Add D-Bus object exports benchmarks Will be used to track memory. --- test/benchmarks/bench_dbus_object_export.py | 78 +++++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 test/benchmarks/bench_dbus_object_export.py diff --git a/test/benchmarks/bench_dbus_object_export.py b/test/benchmarks/bench_dbus_object_export.py new file mode 100644 index 0000000..24e0cad --- /dev/null +++ b/test/benchmarks/bench_dbus_object_export.py @@ -0,0 +1,78 @@ +# 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 DbusInterfaceCommonAsync, dbus_method_async + + +class ExampleInterface( + DbusInterfaceCommonAsync, + interface_name="org.example.interface" +): + @dbus_method_async( + input_signature="s", + result_signature="s", + ) + async def upper(self, string: str) -> str: + return string.upper() + + +def bench_dbus_object_export_stop(loops: int) -> float: + with _isolated_dbus() as bus: + example_object = ExampleInterface() + + start = perf_counter() + + for _ in range(loops): + example_object = ExampleInterface() + handle = example_object.export_to_dbus("/", bus=bus) + handle.stop() + + return perf_counter() - start + + +def bench_dbus_object_export_gc(loops: int) -> float: + with _isolated_dbus() as bus: + example_object = ExampleInterface() + + start = perf_counter() + + for _ in range(loops): + example_object = ExampleInterface() + example_object.export_to_dbus("/", bus=bus) + + return perf_counter() - start + + +def main() -> None: + runner = pyperf.Runner() + runner.bench_time_func("sdbus_dbus_object_export_stop", + bench_dbus_object_export_stop) + runner.bench_time_func("sdbus_dbus_object_export_gc", + bench_dbus_object_export_gc) + + +if __name__ == "__main__": + main() From 26a7100208e1bca2c7bd12cfc0a15cc6ae7e5dbc Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sat, 20 Dec 2025 14:09:40 +0000 Subject: [PATCH 185/188] Fix SIGSEGV if DbusExportHandle outlives exported object Avoid exposing `SdBusInterface.slot` because the slot object depends on `SdBusInterface->vtable`. Instead provide a method to stop exporting. --- src/sdbus/dbus_proxy_async_interface_base.py | 10 ++++----- src/sdbus/sd_bus_internals.py | 4 +++- src/sdbus/sd_bus_internals_interface.c | 11 ++++++++-- test/test_sdbus_async.py | 22 ++++++++++++++++++++ 4 files changed, 39 insertions(+), 8 deletions(-) diff --git a/src/sdbus/dbus_proxy_async_interface_base.py b/src/sdbus/dbus_proxy_async_interface_base.py index 2872669..9fdf92a 100644 --- a/src/sdbus/dbus_proxy_async_interface_base.py +++ b/src/sdbus/dbus_proxy_async_interface_base.py @@ -453,11 +453,8 @@ def new_proxy( class DbusExportHandle: def __init__(self, local_meta: DbusLocalObjectMeta): self._tasks = local_meta.tasks - self._dbus_slots: list[SdBusSlot] = [ - i.slot - for i in local_meta.activated_interfaces - if i.slot is not None - ] + self._dbus_slots: list[SdBusSlot] = [] + self._dbus_interfaces = local_meta.activated_interfaces async def __aenter__(self) -> DbusExportHandle: return self @@ -485,5 +482,8 @@ def stop(self) -> None: for task in self._tasks: task.cancel("D-Bus export stopped") + for interface in self._dbus_interfaces: + interface._stop_export() + for slot in self._dbus_slots: slot.close() diff --git a/src/sdbus/sd_bus_internals.py b/src/sdbus/sd_bus_internals.py index 6319c06..2981b17 100644 --- a/src/sdbus/sd_bus_internals.py +++ b/src/sdbus/sd_bus_internals.py @@ -49,7 +49,6 @@ def close(self) -> None: class SdBusInterface: - slot: Optional[SdBusSlot] method_list: list[object] method_dict: dict[bytes, object] property_list: list[object] @@ -86,6 +85,9 @@ def add_signal( ) -> None: raise NotImplementedError(__STUB_ERROR) + def _stop_export(self) -> None: + raise NotImplementedError(__STUB_ERROR) + class SdBusMessage: def append_data(self, signature: str, *args: DbusCompleteTypes) -> None: diff --git a/src/sdbus/sd_bus_internals_interface.c b/src/sdbus/sd_bus_internals_interface.c index ac9ddc4..df31e7c 100644 --- a/src/sdbus/sd_bus_internals_interface.c +++ b/src/sdbus/sd_bus_internals_interface.c @@ -323,16 +323,23 @@ static PyObject* SdBusInterface_create_vtable(SdBusInterfaceObject* self, PyObje Py_RETURN_NONE; } +static PyObject* SdBusInterface_stop_export(SdBusInterfaceObject* self, PyObject* Py_UNUSED(args)) { + Py_XDECREF(self->interface_slot); + self->interface_slot = NULL; + + Py_RETURN_NONE; +} + static PyMethodDef SdBusInterface_methods[] = { {"add_method", (SD_BUS_PY_FUNC_TYPE)SdBusInterface_add_method, SD_BUS_PY_METH, PyDoc_STR("Add method to the D-Bus interface.")}, {"add_property", (SD_BUS_PY_FUNC_TYPE)SdBusInterface_add_property, SD_BUS_PY_METH, PyDoc_STR("Add property to the D-Bus interface.")}, {"add_signal", (SD_BUS_PY_FUNC_TYPE)SdBusInterface_add_signal, SD_BUS_PY_METH, PyDoc_STR("Add signal to the D-Bus interface.")}, {"_create_vtable", (PyCFunction)SdBusInterface_create_vtable, METH_NOARGS, PyDoc_STR("Creates the vtable.")}, + {"_stop_export", (PyCFunction)SdBusInterface_stop_export, METH_NOARGS, PyDoc_STR("Stop exporting object.")}, {NULL, NULL, 0, NULL}, }; -static PyMemberDef SdBusInterface_members[] = {{"slot", T_OBJECT, offsetof(SdBusInterfaceObject, interface_slot), READONLY, NULL}, - {"method_list", T_OBJECT, offsetof(SdBusInterfaceObject, method_list), READONLY, NULL}, +static PyMemberDef SdBusInterface_members[] = {{"method_list", T_OBJECT, offsetof(SdBusInterfaceObject, method_list), READONLY, NULL}, {"method_dict", T_OBJECT, offsetof(SdBusInterfaceObject, method_dict), READONLY, NULL}, {"property_list", T_OBJECT, offsetof(SdBusInterfaceObject, property_list), READONLY, NULL}, {"property_get_dict", T_OBJECT, offsetof(SdBusInterfaceObject, property_get_dict), READONLY, NULL}, diff --git a/test/test_sdbus_async.py b/test/test_sdbus_async.py index f5dff26..4ce7723 100644 --- a/test/test_sdbus_async.py +++ b/test/test_sdbus_async.py @@ -1008,6 +1008,28 @@ async def test_export_handle(self) -> None: with self.assertRaises(DbusUnknownObjectError): await test_object_connection.returns_none_method() + async def test_export_handle_lifetime(self) -> None: + test_object = TestInterface() + test_object_connection = TestInterface.new_proxy( + TEST_SERVICE_NAME, '/', + ) + handle = test_object.export_to_dbus("/") + await test_object_connection.returns_none_method() + + del test_object + + handle.stop() + + with self.assertRaises(DbusUnknownObjectError): + await test_object_connection.returns_none_method() + + # Test idempotency + handle.stop() + handle.stop() + + with self.assertRaises(DbusUnknownObjectError): + await test_object_connection.returns_none_method() + def test_asyncio_run_different_loops(self) -> None: bus = self.bus From 772961db06dcbe189f1001cf1a4feaea9485e33c Mon Sep 17 00:00:00 2001 From: igo95862 Date: Sun, 21 Dec 2025 20:08:43 +0000 Subject: [PATCH 186/188] Version 0.14.2 --- CHANGELOG.md | 7 +++++++ setup.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e66c69..5c9a348 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## 0.14.2 + +### Fixes + +* Fix segmentation fault if export handle outlives the exported object. (reported by @arkq) +* Fix some tests failing on slow systems. + ## 0.14.1 ### Features diff --git a/setup.py b/setup.py index 5b32c5b..199b300 100644 --- a/setup.py +++ b/setup.py @@ -96,7 +96,7 @@ def get_link_arguments() -> list[str]: 'Based on sd-bus from libsystemd.'), long_description=long_description, long_description_content_type='text/markdown', - version='0.14.1.post0', + version='0.14.2', url='https://github.com/python-sdbus/python-sdbus', author='igo95862', author_email='igo95862@yandex.ru', From 8b03330e6648dc43fceaf2a4fb6c5a4409395b6d Mon Sep 17 00:00:00 2001 From: Beaverr Date: Sat, 11 Apr 2026 13:51:57 -0500 Subject: [PATCH 187/188] Fix generated code for signals --- src/sdbus/interface_generator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sdbus/interface_generator.py b/src/sdbus/interface_generator.py index 27aca92..12bb577 100644 --- a/src/sdbus/interface_generator.py +++ b/src/sdbus/interface_generator.py @@ -722,7 +722,7 @@ def {{ a_property.python_name }}(self) -> {{ a_property.typing }}: flags={{ signal.flags_str }}, {% endif %} {% if signal.wants_rename %} - signal_name=signal.method_name, + signal_name="{{signal.method_name}}", {% endif %} ) def {{ signal.python_name }}(self) -> {{ signal.typing }}: From 90432a805e6cc6f4135f26c9efa755191e3fc33a Mon Sep 17 00:00:00 2001 From: Leopold Luley Date: Sat, 8 Aug 2026 23:24:14 +0200 Subject: [PATCH 188/188] Fix returned file descriptors data type being closed immediatly According to libsystemd man page the returned file descriptor is owned by the message and must be duplicated before being used elsewhere. https://man.archlinux.org/man/core/systemd-libs/sd_bus_message_read_basic.3.en --------- Co-authored-by: Leopold Luley --- src/sdbus/sd_bus_internals_message.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/sdbus/sd_bus_internals_message.c b/src/sdbus/sd_bus_internals_message.c index f5631b3..71d9b0e 100644 --- a/src/sdbus/sd_bus_internals_message.c +++ b/src/sdbus/sd_bus_internals_message.c @@ -18,6 +18,7 @@ License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ +#include #include "sd_bus_internals.h" void _SdBusMessage_set_messsage(SdBusMessageObject* self, sd_bus_message* new_message) { @@ -800,6 +801,10 @@ static PyObject* _iter_basic(sd_bus_message* message, char basic_type) { case 'h': { int new_fd = 0; CALL_SD_BUS_AND_CHECK(sd_bus_message_read_basic(message, basic_type, &new_fd)); + + // The fd is owned by the message and would be closed after the end of the message's lifetime + new_fd = CALL_SD_BUS_AND_CHECK(fcntl(new_fd, F_DUPFD_CLOEXEC, 3)); + return PyLong_FromLong((long)new_fd); break; }