diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
new file mode 100644
index 0000000..3cb7e1d
--- /dev/null
+++ b/.github/FUNDING.yml
@@ -0,0 +1,2 @@
+patreon: igo95862
+liberapay: igo95862
diff --git a/.github/workflows/ubuntu_pypi_test.yml b/.github/workflows/ubuntu_pypi_test.yml
index 07c0724..495bc7f 100644
--- a/.github/workflows/ubuntu_pypi_test.yml
+++ b/.github/workflows/ubuntu_pypi_test.yml
@@ -1,26 +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-20.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@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
+ 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>=0.8rc2
+ 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
diff --git a/.github/workflows/ubuntu_test.yml b/.github/workflows/ubuntu_test.yml
index 3b496de..6a028ea 100644
--- a/.github/workflows/ubuntu_test.yml
+++ b/.github/workflows/ubuntu_test.yml
@@ -2,17 +2,18 @@
name: CI
on:
push:
- branches: [master]
pull_request:
workflow_dispatch:
+ schedule:
+ - cron: '0 0 * * 5'
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@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
+ uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11
- name: Install dependencies
run: |
sudo apt update
@@ -26,10 +27,10 @@ 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@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
+ uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11
- name: Install dependencies
run: |
sudo apt update
@@ -48,29 +49,32 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
- uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
+ uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11
- 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<8.0' 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
steps:
- name: Checkout
- uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
+ uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11
- name: Build Alpine container
run: |
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
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/.readthedocs.yaml b/.readthedocs.yaml
index 2112b9c..2ad865e 100644
--- a/.readthedocs.yaml
+++ b/.readthedocs.yaml
@@ -2,5 +2,14 @@
version: 2
+build:
+ os: "ubuntu-22.04"
+ tools:
+ python: "3.9"
+
+sphinx:
+ configuration: "docs/conf.py"
+
python:
- version: 3.8
+ install:
+ - requirements: docs/requirements.txt
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 675fc27..5c9a348 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,207 @@
+## 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
+
+* 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
+
+* 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
+
+* 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.
+
+## 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:
+
+* 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:
+
+* 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:
diff --git a/DEPRECATIONS.md b/DEPRECATIONS.md
index 1f7e239..0da80d0 100644
--- a/DEPRECATIONS.md
+++ b/DEPRECATIONS.md
@@ -1,5 +1,23 @@
# 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**: 0.14.0
+
+## 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/README.md b/README.md
index aa22e25..da37713 100644
--- a/README.md
+++ b/README.md
@@ -1,9 +1,12 @@
-[](https://lgtm.com/projects/g/igo95862/python-sdbus/alerts/)
-[](https://lgtm.com/projects/g/igo95862/python-sdbus/context:python)
[](https://python-sdbus.readthedocs.io/en/latest/?badge=latest)
+[](https://pypi.org/project/sdbus/)
# Modern Python library for D-Bus
+
+
+
+
Features:
* Asyncio and blocking calls.
@@ -11,7 +14,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)
@@ -26,17 +29,34 @@ 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))
+* [modemmanager](https://github.com/zhanglongqi/python-sdbus-modemmanager) (by [@zhanglongqi](https://github.com/zhanglongqi))
+
+## 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
-* 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.
@@ -47,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
@@ -141,7 +161,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('/')
@@ -180,10 +200,16 @@ 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())
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.
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_api.rst b/docs/asyncio_api.rst
index ab14b33..3411f2f 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.
@@ -68,71 +68,109 @@ 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.
+ :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:
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)
- 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.
+
+ 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.
+
+ 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("/"):
+ # 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.
: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.
+
+ :return: Handle to control the export.
.. py:class:: DbusObjectManagerInterfaceAsync(interface_name)
@@ -147,47 +185,56 @@ 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:
Get the objects this object manager in managing.
+ :py:func:`sdbus.utils.parse.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.
- 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.
+ :py:func:`sdbus.utils.parse.parse_interfaces_added` can be used
+ to make signal data easier to work with.
+
Signal data is:
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.
+ :py:func:`sdbus.utils.parse.parse_interfaces_removed` can be used
+ to make signal data easier to work with.
+
Signal data is:
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)
@@ -202,6 +249,36 @@ Classes
ObjectManager will keep the reference to the object.
+ 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
+
+ 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.
@@ -209,10 +286,11 @@ 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.
+ :return: Handle to control the export.
.. py:method:: remove_managed_object(managed_object)
@@ -239,11 +317,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.
@@ -281,7 +359,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: ::
@@ -300,7 +378,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()
@@ -309,7 +387,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.
@@ -322,7 +400,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.
@@ -334,29 +412,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: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
@@ -386,13 +441,52 @@ 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.
+
+ *Changed in version 0.12.0:* can now be used in overrides.
+
+ .. 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 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.
@@ -416,58 +510,62 @@ Decorators
:param str signal_name: Forces specific signal name instead
of being based on Python function name.
- Signals have following methods:
+ Example::
+
+ from sdbus import DbusInterfaceCommonAsync, dbus_signal_async
- .. py:method:: catch()
- Catch D-Bus signals using the async generator for loop:
- ``async for x in something.some_signal.catch():``
+ class ExampleInterface(DbusInterfaceCommonAsync,
+ interface_name='org.example.signal'
+ ):
- This is main way to await for new events.
+ @dbus_signal_async('s')
+ def name_changed(self) -> str:
+ raise NotImplementedError
- Both remote and local objects operate the same way.
+ .. py:class:: DbusSignalAsync
- Signal objects can also be async iterated directly:
- ``async for x in something.some_signal``
+ Signals have following methods:
- .. py:method:: catch_anywhere(service_name, bus)
+ .. py:method:: catch()
- Catch signal independent of path.
- Yields tuple of path of the object that emitted signal and signal data.
+ Catch D-Bus signals using the async generator for loop:
+ ``async for x in something.some_signal.catch():``
- ``async for path, data in something.some_signal.catch_anywhere():``
+ This is main way to await for new events.
- This method can be called from both an proxy object and class.
- However, it cannot be called on local objects and will raise
- ``NotImplementedError``.
+ Both remote and local objects operate the same way.
- :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.
+ Signal objects can also be async iterated directly:
+ ``async for x in something.some_signal``
- :param str bus:
- Optional dbus 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:: catch_anywhere(service_name, bus)
- .. py:method:: emit(args)
+ Catch signal independent of path.
+ Yields tuple of path of the object that emitted signal and signal data.
- Emit a new signal with *args* data.
+ ``async for path, data in something.some_signal.catch_anywhere():``
- Example: ::
+ This method can be called from both an proxy object and class.
+ However, it cannot be called on local objects and will raise
+ ``NotImplementedError``.
- from sdbus import DbusInterfaceCommonAsync, dbus_signal_async
+ :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
new file mode 100644
index 0000000..5e433d8
--- /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/asyncio_quick.rst b/docs/asyncio_quick.rst
index dac3c01..070f245 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: ::
@@ -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
@@ -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: ::
@@ -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`.
@@ -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()
@@ -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.
@@ -265,28 +265,28 @@ 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::
- 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
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-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.
@@ -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 accf77c..21d27ff 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.
@@ -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
@@ -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.
+ def features(self) -> list[str]:
+ """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/code_generator.rst b/docs/code_generator.rst
index 950a53e..dfe31d5 100644
--- a/docs/code_generator.rst
+++ b/docs/code_generator.rst
@@ -3,15 +3,19 @@ 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 `_
+`Jinja `_
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
-------------------------
@@ -47,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/docs/common_api.rst b/docs/common_api.rst
index e61eea4..3c39f58 100644
--- a/docs/common_api.rst
+++ b/docs/common_api.rst
@@ -3,57 +3,37 @@ Common API
These calls are shared between async and blocking API.
-.. py:currentmodule:: sdbus
-
-Dbus connections calls
-++++++++++++++++++++++++++++++++++
-
-.. py:function:: request_default_bus_name_async(new_name)
- :async:
-
- Acquire a name on the default bus async.
-
- :param str new_name: the name to acquire.
- Must be a valid dbus service name.
+Default bus
++++++++++++
-.. py:function:: request_default_bus_name(new_name)
+.. automodule:: sdbus.default_bus
+ :members:
- Acquire a name on the default bus.
-
- :param str new_name: the name to acquire.
- Must be a valid dbus service name.
-
-.. 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)
@@ -64,7 +44,7 @@ Dbus 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)
@@ -74,7 +54,7 @@ Dbus 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)
@@ -84,7 +64,7 @@ Dbus 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
@@ -100,13 +80,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/conf.py b/docs/conf.py
index ab64486..b165939 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -25,9 +25,9 @@
author = 'igo95862'
source_suffix = '.rst'
extensions = ['sdbus.autodoc']
+html_theme = "sphinx_rtd_theme"
autoclass_content = 'both'
-autodoc_typehints = 'description'
autodoc_member_order = 'bysource'
path.insert(0, abspath('../src'))
diff --git a/docs/examples.rst b/docs/examples.rst
index 7ef60c2..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: ::
@@ -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 d9e4cdf..35d2bd2 100644
--- a/docs/exceptions.rst
+++ b/docs/exceptions.rst
@@ -1,10 +1,12 @@
Exceptions
========================
+.. py:currentmodule:: sdbus.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.
@@ -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
@@ -202,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
@@ -252,7 +280,7 @@ Error name exception list
.. py:exception:: DbusUnknownMethodError
- Unknown dbus method.
+ Unknown D-Bus method.
.. py:attribute:: dbus_error_name
:type: str
@@ -260,7 +288,7 @@ Error name exception list
.. py:exception:: DbusUnknownObjectError
- Unknown dbus object.
+ Unknown D-Bus object.
.. py:attribute:: dbus_error_name
:type: str
@@ -268,7 +296,7 @@ Error name exception list
.. py:exception:: DbusUnknownInterfaceError
- Unknown dbus interface.
+ Unknown D-Bus interface.
.. py:attribute:: dbus_error_name
:type: str
@@ -276,7 +304,7 @@ Error name exception list
.. py:exception:: DbusUnknownPropertyError
- Unknown dbus property.
+ Unknown D-Bus property.
.. py:attribute:: dbus_error_name
:type: str
@@ -284,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
@@ -300,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
@@ -316,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..1a61eec 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,79 +58,79 @@ 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.
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,29 +139,35 @@ 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
+++++++++++++++++++++
-* **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 0c8a3dd..21b5646 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-Spy D-Bus debugger and observe services and objects on your D-Bus `_
.. toctree::
@@ -37,7 +37,9 @@ 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
proxies
code_generator
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
diff --git a/docs/sync_api.rst b/docs/sync_api.rst
index a3ce8e8..641949e 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.
@@ -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,21 +124,21 @@ 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
+++++++++++++++
.. 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.
@@ -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::
@@ -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 6c9e695..23f0ccc 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
@@ -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``.
@@ -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: ::
@@ -128,12 +128,12 @@ 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
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-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.
@@ -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 d7f5364..ab0a200 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/docs/utils.rst b/docs/utils.rst
new file mode 100644
index 0000000..f04ccde
--- /dev/null
+++ b/docs/utils.rst
@@ -0,0 +1,21 @@
+Utilities
+=========
+
+Parsing utilities
++++++++++++++++++
+
+Parse unweildy D-Bus structures in to Python native objects and names.
+Available under ``sdbus.utils.parse`` subpackage.
+
+.. automodule:: sdbus.utils.parse
+ :members:
+
+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.
+
+.. automodule:: sdbus.utils.inspect
+ :members:
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())
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()
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/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"
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/setup.py b/setup.py
index a3ad61f..199b300 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:
@@ -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'):
@@ -52,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,
@@ -65,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
@@ -74,12 +77,12 @@ def get_link_arguments() -> List[str]:
link_arguments.append('-flto')
-compile_arguments: List[str] = ['-flto']
+compile_arguments: list[str] = ['-flto']
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
@@ -93,16 +96,16 @@ 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',
- url='https://github.com/igo95862/python-sdbus',
+ version='0.14.2',
+ 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',
@@ -115,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',
@@ -137,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',
diff --git a/src/sdbus/__init__.py b/src/sdbus/__init__.py
index 28583f9..0e0f7b8 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,
@@ -57,15 +52,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,
@@ -77,6 +70,13 @@
)
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_context_default_bus,
+ set_default_bus,
+)
from .sd_bus_internals import (
DbusDeprecatedFlag,
DbusHiddenFlag,
@@ -103,9 +103,6 @@
)
__all__ = (
- 'get_default_bus', 'request_default_bus_name',
- 'request_default_bus_name_async', 'set_default_bus',
-
'DbusAccessDeniedError', 'DbusAddressInUseError',
'DbusAuthFailedError', 'DbusBadAddressError',
'DbusDisconnectedError', 'DbusFailedError',
@@ -146,6 +143,12 @@
'dbus_property',
+ "get_default_bus",
+ "request_default_bus_name",
+ "request_default_bus_name_async",
+ "set_context_default_bus",
+ "set_default_bus",
+
'DbusDeprecatedFlag',
'DbusHiddenFlag',
'DbusNoReplyFlag',
diff --git a/src/sdbus/__main__.py b/src/sdbus/__main__.py
index 7081468..d9ad8ae 100644
--- a/src/sdbus/__main__.py
+++ b/src/sdbus/__main__.py
@@ -19,70 +19,370 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
from __future__ import annotations
-from argparse import ArgumentParser, Namespace
+from argparse import SUPPRESS, Action, ArgumentParser
+from dataclasses import dataclass, field
from pathlib import Path
-from typing import List
+from sys import stdout
+from typing import TYPE_CHECKING
from .interface_generator import (
- DbusInterfaceIntrospection,
- generate_async_py_file,
+ generate_py_file,
interfaces_from_file,
interfaces_from_str,
)
+if TYPE_CHECKING:
+ from typing import Optional
-def run_gen_from_connection(namespace: Namespace) -> None:
- connection_name = namespace.connection_name
- object_paths = namespace.object_paths
+ 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],
+ system: bool,
+ imports_header: bool,
+ do_async: bool,
+) -> None:
+ connection_name = connection_name
+ object_paths = object_paths
from .dbus_proxy_sync_interfaces import DbusInterfaceCommon
- if namespace.system:
- from .dbus_common_funcs import set_default_bus
+ if system:
+ from .default_bus import set_default_bus
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()
interfaces.extend(interfaces_from_str(itrospection))
- print(
- generate_async_py_file(
- interfaces, namespace.no_imports_header))
+ rename_interfaces(interfaces)
+
+ stdout.write(
+ generate_py_file(
+ interfaces,
+ imports_header,
+ do_async,
+ )
+ )
-def run_gen_from_file(namespace: Namespace) -> None:
- interfaces: List[DbusInterfaceIntrospection] = []
+def run_gen_from_file(
+ filenames: list[str],
+ imports_header: bool,
+ do_async: bool,
+) -> None:
+ interfaces: list[DbusInterfaceIntrospection] = []
- for file in namespace.filenames:
+ for file in filenames:
interfaces.extend(interfaces_from_file(file))
- print(
- generate_async_py_file(
- interfaces, namespace.no_imports_header))
+ rename_interfaces(interfaces)
+ stdout.write(
+ generate_py_file(
+ interfaces,
+ imports_header,
+ do_async,
+ )
+ )
-def generator_main() -> None:
- main_arg_parser = ArgumentParser()
- subparsers = main_arg_parser.add_subparsers()
+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}"
+ )
- generate_from_file_parser = subparsers.add_parser('gen-from-file')
- generate_from_file_parser.set_defaults(func=run_gen_from_file)
+ interface_rename = rename_root.interfaces.get(values)
- generate_from_file_parser.add_argument(
- 'filenames', type=Path, nargs='+')
+ if interface_rename is None:
+ interface_rename = RenameInterface()
+ rename_root.interfaces[values] = interface_rename
- generate_from_file_parser.add_argument(
- '--no-imports-header', action='store_false', default=True,
- help="Do NOT include 'import' header",
+ 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(
+ 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)
+
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)",
+ )
+
+ 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",
+ )
+ 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='+',
+ help="Paths to interface XML introspection files"
+ )
+
generate_from_connection.add_argument(
'connection_name',
help=(
@@ -98,18 +398,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__":
diff --git a/src/sdbus/autodoc.py b/src/sdbus/autodoc.py
index 8a709ea..8b4a918 100644
--- a/src/sdbus/autodoc.py
+++ b/src/sdbus/autodoc.py
@@ -17,38 +17,36 @@
# 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
-from .dbus_proxy_async_property import (
- DbusPropertyAsync,
- DbusPropertyAsyncBinded,
-)
-from .dbus_proxy_async_signal import DbusSignalAsync, DbusSignalBinded
+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
+
+ from sphinx.application import Sphinx
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, 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,
@@ -65,20 +63,13 @@ def add_content(self,
class DbusPropertyDocumenter(AttributeDocumenter):
- objtype = 'DbusPropertyAsyncBinded'
+ objtype = 'DbusPropertyAsync'
directivetype = 'attribute'
priority = 100 + AttributeDocumenter.priority
@classmethod
def can_document_member(cls, member: Any, *args: Any) -> bool:
- return isinstance(member, DbusPropertyAsyncBinded)
-
- 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,
@@ -106,20 +97,13 @@ def add_content(self,
class DbusSignalDocumenter(AttributeDocumenter):
- objtype = 'DbusSignalBinded'
+ objtype = 'DbusSignalAsync'
directivetype = 'attribute'
priority = 100 + AttributeDocumenter.priority
@classmethod
def can_document_member(cls, member: Any, *args: Any) -> bool:
- return isinstance(member, DbusSignalBinded)
-
- 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,
@@ -145,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 2dd19e9..4df138f 100644
--- a/src/sdbus/dbus_common_elements.py
+++ b/src/sdbus/dbus_common_elements.py
@@ -20,59 +20,78 @@
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, Generic, TypeVar
from .dbus_common_funcs import (
_is_property_flags_correct,
- _method_name_converter,
+ 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:
+ from asyncio import Task
+ from collections.abc import Callable, Sequence
+ from types import FunctionType
+ from typing import Any, Optional
+
+ SelfMeta = TypeVar('SelfMeta', bound="DbusInterfaceMetaCommon")
+
+ from .sd_bus_internals import SdBus, SdBusInterface
+
+T = TypeVar('T')
-class DbusSomethingCommon:
- def __init__(self) -> None:
- self.interface_name: Optional[str] = None
- self.serving_enabled: bool = True
+class DbusMemberCommon:
+ interface_name: str
+ serving_enabled: bool
-class DbusSomethingAsync(DbusSomethingCommon):
+
+class DbusMemberAsync(DbusMemberCommon):
...
-class DbusSomethingSync(DbusSomethingCommon):
+class DbusMemberSync(DbusMemberCommon):
...
class DbusInterfaceMetaCommon(type):
- def __new__(cls, 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,
- ) -> DbusInterfaceMetaCommon:
+ ) -> SelfMeta:
if interface_name is not None:
try:
assert is_interface_name_valid(interface_name), (
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.'
)
except NotImplementedError:
...
+ for attr_name, attr in namespace.items():
+ if not isinstance(attr, DbusMemberCommon):
+ 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
@@ -86,16 +105,16 @@ def __new__(cls, name: str,
)
-class DbusMethodCommon(DbusSomethingCommon):
+class DbusMethodCommon(DbusMemberCommon):
def __init__(
self,
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), (
@@ -103,17 +122,8 @@ 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__))
+ method_name = snake_case_to_camel_case(original_method.__name__)
try:
assert is_member_name_valid(method_name), (
@@ -136,13 +146,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__
@@ -151,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
@@ -174,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:
@@ -201,15 +223,14 @@ def _rebuild_args(
return new_args_list
-class DbusPropertyCommon(DbusSomethingCommon):
+class DbusPropertyCommon(DbusMemberCommon):
def __init__(self,
property_name: Optional[str],
property_signature: str,
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), (
@@ -232,7 +253,7 @@ def __init__(self,
self.flags = flags
-class DbusSingalCommon(DbusSomethingCommon):
+class DbusSignalCommon(DbusMemberCommon):
def __init__(self,
signal_name: Optional[str],
signal_signature: str,
@@ -240,8 +261,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), (
@@ -261,21 +281,75 @@ def __init__(self,
self.__annotations__ = original_method.__annotations__
-class DbusBindedAsync:
+class DbusBoundAsync:
...
-class DbusBindedSync:
+class DbusBoundSync:
...
-T = TypeVar('T')
+class DbusMethodOverride(Generic[T]):
+ def __init__(self, override_method: T):
+ self.override_method = override_method
-class DbusOverload:
- def __init__(self, original: T):
- self.original = original
- self.setter_overload: Optional[Callable[[Any, T], None]] = None
+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
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_override = new_setter
+ self.is_setter_public = False
+
+
+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 DbusLocalObjectMeta:
+ 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:
+ 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_common_funcs.py b/src/sdbus/dbus_common_funcs.py
index 486cad7..08a7f4c 100644
--- a/src/sdbus/dbus_common_funcs.py
+++ b/src/sdbus/dbus_common_funcs.py
@@ -1,7 +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,19 +20,19 @@
from __future__ import annotations
from asyncio import get_running_loop
-from contextvars import ContextVar
-from typing import Iterator
+from typing import TYPE_CHECKING
from .sd_bus_internals import (
DbusPropertyConstFlag,
DbusPropertyEmitsChangeFlag,
DbusPropertyEmitsInvalidationFlag,
DbusPropertyExplicitFlag,
- SdBus,
- sd_bus_open,
)
-DEFAULT_BUS: ContextVar[SdBus] = ContextVar('DEFAULT_BUS')
+if TYPE_CHECKING:
+ from collections.abc import Iterator, Mapping
+ from typing import Any, Literal
+
PROPERTY_FLAGS_MASK = (
DbusPropertyConstFlag | DbusPropertyEmitsChangeFlag |
@@ -50,35 +49,8 @@ def _is_property_flags_correct(flags: int) -> bool:
return (0 <= num_of_flag_bits <= 1)
-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,
- flags: int = 0,) -> None:
- default_bus = get_default_bus()
- await default_bus.request_name_async(new_name, flags)
-
-
-async def request_default_bus_name(
- new_name: str,
- flags: int = 0,) -> None:
- default_bus = get_default_bus()
- default_bus.request_name(new_name, flags)
-
-
-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)
@@ -100,9 +72,39 @@ 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()
return False
except RuntimeError:
return True
+
+
+def _parse_properties_vardict(
+ properties_name_map: Mapping[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_exceptions.py b/src/sdbus/dbus_exceptions.py
index afc9358..b178503 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,22 +27,30 @@
map_exception_to_dbus_error,
)
+if TYPE_CHECKING:
+ from typing import Any
+
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],
+ ) -> type[Exception]:
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)
+ 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 cc19dac..9fdf92a 100644
--- a/src/sdbus/dbus_proxy_async_interface_base.py
+++ b/src/sdbus/dbus_proxy_async_interface_base.py
@@ -19,158 +19,288 @@
# 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 collections.abc import Callable
+from copy import copy
+from itertools import chain
+from typing import TYPE_CHECKING, Any, cast
from warnings import warn
-from weakref import ref as weak_ref
+from weakref import WeakKeyDictionary, WeakValueDictionary
from .dbus_common_elements import (
- DbusBindedAsync,
+ DbusClassMeta,
DbusInterfaceMetaCommon,
- DbusOverload,
- DbusSomethingAsync,
- DbusSomethingSync,
+ DbusLocalObjectMeta,
+ DbusMemberAsync,
+ DbusMemberCommon,
+ DbusMemberSync,
+ DbusMethodOverride,
+ DbusPropertyOverride,
+ DbusRemoteObjectMeta,
)
-from .dbus_common_funcs import get_default_bus
-from .dbus_proxy_async_method import DbusMethodAsync, DbusMethodAsyncBinded
+from .dbus_proxy_async_method import DbusLocalMethodAsync, DbusMethodAsync
from .dbus_proxy_async_property import (
+ DbusLocalPropertyAsync,
DbusPropertyAsync,
- DbusPropertyAsyncBinded,
)
-from .dbus_proxy_async_signal import DbusSignalAsync, DbusSignalBinded
-from .sd_bus_internals import SdBus, SdBusInterface
+from .dbus_proxy_async_signal import DbusLocalSignalAsync, DbusSignalAsync
+from .default_bus import get_default_bus
+from .sd_bus_internals import SdBusInterface
+
+if TYPE_CHECKING:
+ from collections.abc import Iterable, Iterator
+ from typing import Optional, TypeVar, Union
+
+ from .sd_bus_internals import SdBus, SdBusSlot
+
+ T = TypeVar('T')
+ Self = TypeVar('Self', bound="DbusInterfaceBaseAsync")
+ DbusOverride = Union[DbusMethodOverride[T], DbusPropertyOverride[T]]
-T_input = TypeVar('T_input')
+
+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_served_interfaces_names = (
- {interface_name}
- if serving_enabled and interface_name is not None
- else set()
+ @staticmethod
+ def _process_dbus_method_override(
+ override_attr_name: str,
+ override: DbusMethodOverride[T],
+ mro_dbus_elements: dict[str, DbusMemberAsync],
+ ) -> DbusMethodAsync:
+ try:
+ 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_dbus_method, DbusMethodAsync):
+ raise TypeError(
+ f"Expected {DbusMethodAsync!r} got {original_dbus_method!r} "
+ f"under name {override_attr_name!r}"
+ )
+
+ new_method = copy(original_dbus_method)
+ new_method.original_method = (
+ override.override_method # type: ignore[assignment]
)
- dbus_to_python_name_map: Dict[str, str] = {}
- dbus_declared_members: Dict[str, DbusSomethingAsync] = {}
- superclass_members: Dict[str, DbusSomethingAsync] = {}
-
- for base in bases:
- if issubclass(base, DbusInterfaceBaseAsync):
- dbus_to_python_name_map.update(
- base._dbus_to_python_name_map
+ return new_method
+
+ @staticmethod
+ def _process_dbus_property_override(
+ override_attr_name: str,
+ override: DbusPropertyOverride[T],
+ mro_dbus_elements: dict[str, DbusMemberAsync],
+ ) -> 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
+
+ @classmethod
+ def _check_collisions(
+ cls,
+ new_class_name: str,
+ namespace: dict[str, Any],
+ mro_dbus_elements: dict[str, DbusMemberAsync],
+ ) -> None:
+
+ possible_collisions = namespace.keys() & mro_dbus_elements.keys()
+ new_overrides: dict[str, DbusMemberAsync] = {}
+
+ for attr_name, attr in namespace.items():
+ if isinstance(attr, DbusMethodOverride):
+ new_overrides[attr_name] = cls._process_dbus_method_override(
+ attr_name,
+ attr,
+ mro_dbus_elements,
)
- dbus_served_interfaces_names.update(
- base._dbus_served_interfaces_names
+ 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 possible_collisions:
+ raise ValueError(
+ f"Interface {new_class_name!r} redefines reserved "
+ f"D-Bus attribute names: {possible_collisions!r}"
+ )
- superclass_members.update(
- base._dbus_declared_members
+ namespace.update(new_overrides)
+
+ @staticmethod
+ def _extract_dbus_elements(
+ dbus_class: type,
+ dbus_meta: DbusClassMeta,
+ ) -> 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, DbusMemberAsync):
+ raise TypeError(
+ f"Expected async D-Bus element, got {dbus_element!r} "
+ f"in class {dbus_class!r}"
)
- for key, value in namespace.items():
- assert not isinstance(value, DbusSomethingSync), (
- "Can't mix sync methods in async interface."
+ dbus_elements_map[attr_name] = dbus_element
+
+ return dbus_elements_map
+
+ @classmethod
+ 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()
+
+ for c in base_classes:
+ dbus_meta = DBUS_CLASS_TO_META.get(c)
+ if dbus_meta is None:
+ continue
+
+ base_dbus_elements = cls._extract_dbus_elements(c, dbus_meta)
+
+ possible_collisions.update(
+ base_dbus_elements.keys() & all_python_dbus_map.keys()
)
- if isinstance(value, DbusSomethingAsync):
- value.interface_name = interface_name
- value.serving_enabled = serving_enabled
- dbus_declared_members[key] = value
-
- 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
-
- try:
- super_dbus_def = superclass_members[key]
- except KeyError:
- if isinstance(value, DbusOverload):
- raise TypeError(
- f"No D-Bus member '{key}' to overload with."
- )
- else:
- if not isinstance(value, DbusOverload):
- raise TypeError(
- "Attempted to overload dbus definition"
- " without using @dbus_overload decorator"
- )
+ all_python_dbus_map.update(
+ base_dbus_elements
+ )
- if isinstance(super_dbus_def, DbusMethodAsync):
- new_method_def = deepcopy(super_dbus_def)
- new_method_def.original_method = cast(
- MethodType, value.original)
-
- namespace[key] = new_method_def
- elif isinstance(super_dbus_def, DbusPropertyAsync):
- new_property_def = deepcopy(super_dbus_def)
- new_property_def.property_getter = cast(
- Callable[[DbusInterfaceBaseAsync], Any],
- value.original)
- if value.setter_overload is not None:
- new_property_def.property_setter = (
- value.setter_overload
- )
-
- namespace[key] = new_property_def
- else:
- raise TypeError('Unknown D-Bus overload')
+ 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_map
- dbus_declared_members.update(superclass_members)
+ @staticmethod
+ def _map_dbus_elements(
+ attr_name: str,
+ attr: Any,
+ meta: DbusClassMeta,
+ interface_name: str,
+ ) -> None:
+ if not isinstance(attr, DbusMemberCommon):
+ return
+
+ if isinstance(attr, DbusMemberSync):
+ raise TypeError(
+ "Can't mix blocking methods in "
+ f"async interface: {attr_name!r}"
+ )
+
+ 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."
+ )
+
+ 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)
- 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
new_cls = super().__new__(
cls, name, bases, namespace,
interface_name,
serving_enabled,
)
- return cast(DbusInterfaceMetaAsync, new_cls)
+ if interface_name is not None:
+ 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
+
+ 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_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]
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()
+
+ @classmethod
+ def _dbus_iter_interfaces_meta(
+ cls,
+ ) -> Iterator[tuple[str, DbusClassMeta]]:
+
+ for base in reversed(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,
@@ -181,52 +311,42 @@ 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,
- ) -> None:
+ ) -> DbusExportHandle:
+ local_object_meta = self._dbus
+ if isinstance(local_object_meta, DbusRemoteObjectMeta):
+ raise RuntimeError("Cannot export D-Bus proxies.")
- 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
- # 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):
- interface_name = value.dbus_method.interface_name
- if not value.dbus_method.serving_enabled:
- continue
- elif isinstance(value, DbusPropertyAsyncBinded):
- interface_name = value.dbus_property.interface_name
- if not value.dbus_property.serving_enabled:
- continue
- elif isinstance(value, DbusSignalBinded):
- interface_name = value.dbus_signal.interface_name
- if not value.dbus_signal.serving_enabled:
- continue
- else:
- continue
+ if local_object_meta.attached_bus is not None:
+ raise RuntimeError(
+ "Object already exported. "
+ "This limitation should be fixed in future version."
+ )
- assert interface_name is not None
+ if bus is None:
+ bus = get_default_bus()
- try:
- interface_member_list = interface_map[interface_name]
- except KeyError:
- interface_member_list = []
- interface_map[interface_name] = interface_member_list
+ local_object_meta.attached_bus = bus
+ local_object_meta.serving_object_path = object_path
- 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:
- if isinstance(dbus_something, DbusMethodAsyncBinded):
+
+ 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,
dbus_something.dbus_method.input_signature,
@@ -234,24 +354,29 @@ 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
-
- setter = (dbus_something._reply_set_sync
- if dbus_something.dbus_property.property_setter
- is not None
- else None)
+ elif isinstance(dbus_something, DbusLocalPropertyAsync):
+ getter = dbus_something._dbus_reply_get
+ dbus_property = dbus_something.dbus_property
+
+ if (
+ dbus_property.property_setter is not None
+ and
+ dbus_property.property_setter_is_public
+ ):
+ setter = dbus_something._dbus_reply_set
+ 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):
+ elif isinstance(dbus_something, DbusLocalSignalAsync):
new_interface.add_signal(
dbus_something.dbus_signal.signal_name,
dbus_something.dbus_signal.signal_signature,
@@ -259,11 +384,17 @@ def export_to_dbus(
dbus_something.dbus_signal.flags,
)
else:
- raise TypeError
+ raise TypeError(
+ f"Expected D-Bus element, got: {dbus_something!r}"
+ )
+
+ bus.add_interface(new_interface, object_path, interface_name)
+ local_object_meta.activated_interfaces.append(new_interface)
- bus.add_interface(new_interface, object_path,
- interface_name)
- self._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(
self,
@@ -284,39 +415,75 @@ 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(
- 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
+
+
+class DbusExportHandle:
+ def __init__(self, local_meta: DbusLocalObjectMeta):
+ self._tasks = local_meta.tasks
+ self._dbus_slots: list[SdBusSlot] = []
+ self._dbus_interfaces = local_meta.activated_interfaces
+
+ 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 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/dbus_proxy_async_interfaces.py b/src/sdbus/dbus_proxy_async_interfaces.py
index 1b1c809..303aaff 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
@@ -19,15 +19,23 @@
# 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 typing import TYPE_CHECKING
-from .dbus_common_funcs import 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_property import DbusPropertyAsyncBinded
from .dbus_proxy_async_signal import dbus_signal_async
-from .sd_bus_internals import DbusPropertyEmitsChangeFlag, SdBus, SdBusSlot
+
+if TYPE_CHECKING:
+ from typing import Any, Literal
+
+ DBUS_PROPERTIES_CHANGED_TYPING = (
+ tuple[
+ str,
+ dict[str, tuple[str, Any]],
+ list[str],
+ ]
+ )
class DbusPeerInterfaceAsync(
@@ -56,130 +64,48 @@ 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',
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:
- ...
+ raise NotImplementedError
@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:
+ continue
- for interface_name in self._dbus_served_interfaces_names:
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(
+ meta.dbus_member_to_python_attr,
+ dbus_properties_data,
+ on_unknown_member,
+ )
+ )
return properties
class DbusInterfaceCommonAsync(
- DbusPeerInterfaceAsync, DbusPropertiesInterfaceAsync,
- DbusIntrospectableAsync):
+ DbusPropertiesInterfaceAsync,
+ DbusIntrospectableAsync,
+ DbusPeerInterfaceAsync):
...
-
-
-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._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)
diff --git a/src/sdbus/dbus_proxy_async_method.py b/src/sdbus/dbus_proxy_async_method.py
index d42a405..3ac3c2a 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
@@ -19,29 +19,34 @@
# 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
-from typing import (
- TYPE_CHECKING,
- Any,
- Callable,
- Optional,
- Sequence,
- Type,
- TypeVar,
- cast,
-)
+from typing import TYPE_CHECKING, cast, overload
from weakref import ref as weak_ref
from .dbus_common_elements import (
- DbusBindedAsync,
+ DbusBoundAsync,
+ DbusLocalObjectMeta,
+ DbusMemberAsync,
DbusMethodCommon,
- DbusOverload,
- DbusSomethingAsync,
+ DbusMethodOverride,
+ DbusRemoteObjectMeta,
)
from .dbus_exceptions import DbusFailedError
-from .sd_bus_internals import DbusNoReplyFlag, SdBusMessage
+from .sd_bus_internals import EXCEPTION_TO_DBUS_ERROR, DbusNoReplyFlag
+
+if TYPE_CHECKING:
+ 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
+
+ T = TypeVar('T')
+else:
+ T = None
CURRENT_MESSAGE: ContextVar[SdBusMessage] = ContextVar('CURRENT_MESSAGE')
@@ -50,138 +55,175 @@ def get_current_message() -> SdBusMessage:
return CURRENT_MESSAGE.get()
-T_input = TypeVar('T_input')
-T = TypeVar('T')
-
+class DbusMethodAsync(DbusMethodCommon, DbusMemberAsync):
+
+ @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):
+ return DbusProxyMethodAsync(self, dbus_meta)
+ else:
+ return DbusLocalMethodAsync(self, obj)
+ else:
+ return self
-if TYPE_CHECKING:
- from .dbus_proxy_async_interface_base import DbusInterfaceBaseAsync
+class DbusBoundMethodAsyncBase(DbusBoundAsync):
-class DbusMethodAsync(DbusMethodCommon, DbusSomethingAsync):
- def __get__(self,
- obj: DbusInterfaceBaseAsync,
- obj_class: Optional[Type[DbusInterfaceBaseAsync]] = None,
- ) -> Callable[..., Any]:
- return DbusMethodAsyncBinded(self, obj)
+ def __call__(self, *args: Any, **kwargs: Any) -> Any:
+ raise NotImplementedError
-class DbusMethodAsyncBinded(DbusBindedAsync):
- def __init__(self,
- dbus_method: DbusMethodAsync,
- interface: DbusInterfaceBaseAsync):
+class DbusProxyMethodAsync(DbusBoundMethodAsyncBase):
+ 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
- assert self.dbus_method.interface_name 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 DbusLocalMethodAsync(DbusBoundMethodAsyncBase):
+ def __init__(
+ self,
+ dbus_method: DbusMethodAsync,
+ local_object: DbusInterfaceBaseAsync,
+ ):
+ self.dbus_method = dbus_method
+ self.local_object_ref = weak_ref(local_object)
+
+ self.__doc__ = dbus_method.__doc__
- 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)
+ 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._call_dbus_async(*rebuilt_args)
- else:
- return self.dbus_method.original_method(
- interface, *args, **kwargs)
+ return self.dbus_method.original_method(local_object, *args, **kwargs)
- async def _call_method_from_dbus(
- self,
- request_message: SdBusMessage,
- interface: DbusInterfaceBaseAsync) -> Any:
- request_data = request_message.get_contents()
+ async def _dbus_reply_call_method(
+ self,
+ request_message: SdBusMessage,
+ local_object: DbusInterfaceBaseAsync,
+ ) -> Any:
local_method = self.dbus_method.original_method.__get__(
- interface, None)
+ 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)
-
- 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
-
+ return await local_method(*request_message.parse_to_tuple())
+
+ 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!")
+
+ 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._call_method_from_dbus,
+ self._dbus_reply_call_method,
request_message,
- interface,
+ 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
@@ -209,17 +251,17 @@ 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_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 +277,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
@@ -244,6 +286,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_object_manager.py b/src/sdbus/dbus_proxy_async_object_manager.py
new file mode 100644
index 0000000..06d636e
--- /dev/null
+++ b/src/sdbus/dbus_proxy_async_object_manager.py
@@ -0,0 +1,134 @@
+# 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_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
+from .default_bus import get_default_bus
+
+if TYPE_CHECKING:
+ from collections.abc import Callable
+ from typing import Any, Optional
+
+ 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:
+ self.remove_object_call()
+ super().stop()
+
+
+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 _dbus_on_no_members_exported(self) -> None:
+ ... # Object manager is allowed to be exported empty
+
+ 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/dbus_proxy_async_property.py b/src/sdbus/dbus_proxy_async_property.py
index 200a859..bf63f04 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
@@ -19,37 +19,32 @@
# 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,
- Any,
- Callable,
- Generator,
- Generic,
- Optional,
- Type,
- TypeVar,
- cast,
-)
+from typing import TYPE_CHECKING, Generic, TypeVar, cast, overload
from weakref import ref as weak_ref
from .dbus_common_elements import (
- DbusBindedAsync,
- DbusOverload,
+ DbusBoundAsync,
+ DbusMemberAsync,
DbusPropertyCommon,
- DbusSomethingAsync,
+ DbusPropertyOverride,
+ DbusRemoteObjectMeta,
)
-from .sd_bus_internals import SdBusMessage
-
-T = TypeVar('T')
-
if TYPE_CHECKING:
+ 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
-class DbusPropertyAsync(DbusSomethingAsync, DbusPropertyCommon, Generic[T]):
+T = TypeVar('T')
+
+
+class DbusPropertyAsync(DbusMemberAsync, DbusPropertyCommon, Generic[T]):
def __init__(
self,
property_name: Optional[str],
@@ -74,90 +69,193 @@ def __init__(
self.property_setter: Optional[
Callable[[DbusInterfaceBaseAsync, T],
None]] = property_setter
+ self.property_setter_is_public: bool = True
self.__doc__ = property_getter.__doc__
- def __get__(self,
- obj: DbusInterfaceBaseAsync,
- obj_class: Optional[Type[DbusInterfaceBaseAsync]] = None,
- ) -> DbusPropertyAsyncBinded:
- return DbusPropertyAsyncBinded(self, obj)
+ @overload
+ def __get__(
+ self,
+ obj: None,
+ obj_class: type[DbusInterfaceBaseAsync],
+ ) -> DbusPropertyAsync[T]:
+ ...
+
+ @overload
+ def __get__(
+ self,
+ obj: DbusInterfaceBaseAsync,
+ obj_class: type[DbusInterfaceBaseAsync],
+ ) -> DbusBoundPropertyAsyncBase[T]:
+ ...
+
+ def __get__(
+ self,
+ obj: Optional[DbusInterfaceBaseAsync],
+ obj_class: Optional[type[DbusInterfaceBaseAsync]] = None,
+ ) -> Union[DbusBoundPropertyAsyncBase[T], DbusPropertyAsync[T]]:
+ if obj is not None:
+ dbus_meta = obj._dbus
+ if isinstance(dbus_meta, DbusRemoteObjectMeta):
+ return DbusProxyPropertyAsync(self, dbus_meta)
+ else:
+ return DbusLocalPropertyAsync(self, obj)
+ else:
+ return 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
-
-class DbusPropertyAsyncBinded(DbusBindedAsync):
- def __init__(self,
- dbus_property: DbusPropertyAsync[T],
- interface: DbusInterfaceBaseAsync):
- self.dbus_property = dbus_property
- self.interface_ref = (
- weak_ref(interface)
- if interface is not None
- else None
+ 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
- self.__doc__ = dbus_property.__doc__
+class DbusBoundPropertyAsyncBase(DbusBoundAsync, Awaitable[T]):
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
- assert self.dbus_property.interface_name is not None
- new_call_message = interface._attached_bus. \
- new_property_get_message(
- interface._remote_service_name,
- interface._remote_object_path,
+ raise NotImplementedError
+
+ async def set_async(self, complete_object: T) -> None:
+ raise NotImplementedError
+
+
+class DbusProxyPropertyAsync(DbusBoundPropertyAsyncBase[T]):
+ def __init__(
+ self,
+ dbus_property: DbusPropertyAsync[T],
+ proxy_meta: DbusRemoteObjectMeta,
+ ):
+ self.dbus_property = dbus_property
+ self.proxy_meta = proxy_meta
+
+ self.__doc__ = dbus_property.__doc__
+
+ async def get_async(self) -> T:
+ 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)
+
+
+class DbusLocalPropertyAsync(DbusBoundPropertyAsyncBase[T]):
+ def __init__(
+ self,
+ dbus_property: DbusPropertyAsync[T],
+ local_object: DbusInterfaceBaseAsync,
+ ):
+ self.dbus_property = dbus_property
+ self.local_object_ref = weak_ref(local_object)
+
+ 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!")
+
+ 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(
+ 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,
+ complete_object,
+ ),
+ },
+ []
+ )
+ )
+
+ 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!")
- reply_data: Any = self.dbus_property.property_getter(interface)
+ reply_data: Any = self.dbus_property.property_getter(local_object)
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
+ 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!")
assert self.dbus_property.property_setter is not None
data_to_set_to: Any = message.get_contents()
- self.dbus_property.property_setter(interface, data_to_set_to)
+ self.dbus_property.property_setter(local_object, data_to_set_to)
- assert self.dbus_property.interface_name is not None
try:
- properties_changed = getattr(interface, 'properties_changed')
+ properties_changed = getattr(
+ local_object,
+ "properties_changed",
+ )
except AttributeError:
...
else:
@@ -174,38 +272,6 @@ def _reply_set_sync(self, message: SdBusMessage) -> None:
)
)
- 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
-
- if not interface._is_binded:
- if self.dbus_property.property_setter is None:
- raise ValueError('Property has no setter')
-
- self.dbus_property.property_setter(
- interface, complete_object)
-
- return
-
- 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
- assert self.dbus_property.interface_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,
- )
-
- new_call_message.append_data(
- 'v', (self.dbus_property.property_signature, complete_object))
-
- await interface._attached_bus.call_async(new_call_message)
-
def dbus_property_async(
property_signature: str = "",
@@ -225,7 +291,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(
@@ -247,6 +313,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/dbus_proxy_async_signal.py b/src/sdbus/dbus_proxy_async_signal.py
index 7d834cc..e99df9b 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
@@ -20,130 +20,166 @@
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,
- Any,
- AsyncGenerator,
- Callable,
- Generic,
- Optional,
- Sequence,
- Tuple,
- Type,
- TypeVar,
- cast,
-)
-from weakref import ref as weak_ref
+from typing import TYPE_CHECKING, Generic, TypeVar, cast, overload
+from weakref import WeakSet
from .dbus_common_elements import (
- DbusBindedAsync,
- DbusSingalCommon,
- DbusSomethingAsync,
+ DbusBoundAsync,
+ DbusLocalObjectMeta,
+ DbusMemberAsync,
+ DbusRemoteObjectMeta,
+ DbusSignalCommon,
)
-from .dbus_common_funcs import get_default_bus
-from .sd_bus_internals import SdBus, SdBusMessage
-
-T = TypeVar('T')
-
+from .default_bus import get_default_bus
if TYPE_CHECKING:
+ 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
-class DbusSignalAsync(DbusSomethingAsync, DbusSingalCommon, Generic[T]):
+T = TypeVar('T')
- def __get__(self,
- obj: Optional[DbusInterfaceBaseAsync],
- obj_class: Optional[Type[DbusInterfaceBaseAsync]] = None,
- ) -> DbusSignalBinded[T]:
- return DbusSignalBinded(self, obj)
+class DbusSignalAsync(DbusMemberAsync, DbusSignalCommon, Generic[T]):
-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
+ 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.__doc__ = dbus_signal.__doc__
+ 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],
+ ) -> DbusBoundSignalAsyncBase[T]:
+ ...
+
+ def __get__(
+ self,
+ obj: Optional[DbusInterfaceBaseAsync],
+ obj_class: Optional[type[DbusInterfaceBaseAsync]] = None,
+ ) -> Union[DbusBoundSignalAsyncBase[T], DbusSignalAsync[T]]:
+ if obj is not None:
+ dbus_meta = obj._dbus
+ if isinstance(dbus_meta, DbusRemoteObjectMeta):
+ return DbusProxySignalAsync(self, dbus_meta)
+ else:
+ return DbusLocalSignalAsync(self, dbus_meta)
+ else:
+ return 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.interface_name is not None
- assert self.dbus_signal.signal_name is not 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,
- )
+ async def catch_anywhere(
+ self,
+ service_name: str,
+ bus: Optional[SdBus] = None,
+ ) -> AsyncIterable[tuple[str, T]]:
+ if bus is None:
+ bus = get_default_bus()
- def _cleanup_local_queue(
- self,
- queue_ref: weak_ref[Queue[T]]) -> None:
- assert self.interface_ref is not None, (
- "Called method from class?"
+ 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,
)
- interface = self.interface_ref()
- assert interface is not None
- interface._local_signal_queues[self.dbus_signal].remove(queue_ref)
+ 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 _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
+class DbusBoundSignalAsyncBase(DbusBoundAsync, AsyncIterable[T], Generic[T]):
+ async def catch(self) -> AsyncIterator[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
+ __aiter__ = catch
- new_queue: Queue[T] = Queue()
+ async def catch_anywhere(
+ self,
+ service_name: Optional[str] = None,
+ bus: Optional[SdBus] = None,
+ ) -> AsyncIterable[tuple[str, T]]:
+ raise NotImplementedError
+ yield "", cast(T, None)
+
+ def emit(self, args: T) -> None:
+ raise NotImplementedError
- list_of_queues.append(weak_ref(new_queue, self._cleanup_local_queue))
- return new_queue
+class DbusProxySignalAsync(DbusBoundSignalAsyncBase[T]):
+ def __init__(
+ self,
+ dbus_signal: DbusSignalAsync[T],
+ proxy_meta: DbusRemoteObjectMeta,
+ ):
+ self.dbus_signal = dbus_signal
+ self.proxy_meta = proxy_meta
- async def catch(self) -> AsyncGenerator[T, None]:
- assert self.interface_ref is not None, (
- "Called method from class?"
+ self.__doc__ = dbus_signal.__doc__
+
+ 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,
)
- interface = self.interface_ref()
- assert interface is not None
- if interface._is_binded:
- message_queue = await self._get_dbus_queue()
+ 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,
+ )
+ with closing(match_slot):
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
__aiter__ = catch
@@ -151,63 +187,82 @@ async def catch_anywhere(
self,
service_name: Optional[str] = None,
bus: Optional[SdBus] = None,
- ) -> AsyncGenerator[Tuple[str, T], None]:
+ ) -> AsyncIterable[tuple[str, T]]:
+ if bus is None:
+ bus = self.proxy_meta.attached_bus
+
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'
- )
+ service_name = self.proxy_meta.service_name
- 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()
+ message_queue: Queue[SdBusMessage] = Queue()
- message_queue = await bus.get_signal_queue_async(
+ 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())
- )
-
- def _emit_message(self, args: T) -> None:
- assert self.interface_ref is not None, (
- "Called method from class?"
- )
- interface = self.interface_ref()
- assert interface is not None
+ 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.")
- 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(
- interface._serving_object_path,
+class DbusLocalSignalAsync(DbusBoundSignalAsyncBase[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]:
+ new_queue: Queue[T] = 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:
+ signal_callbacks.remove(put_method)
+
+ __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
+
+ 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,
)
@@ -217,6 +272,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)
@@ -224,24 +281,10 @@ 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
-
- if interface._activated_interfaces:
- self._emit_message(args)
-
- try:
- list_of_queues = interface._local_signal_queues[self.dbus_signal]
- except KeyError:
- return
+ self._emit_dbus_signal(args)
- for local_queue_ref in list_of_queues:
- local_queue = local_queue_ref()
- assert local_queue is not None
- local_queue.put_nowait(args)
+ for callback in self.dbus_signal.local_callbacks:
+ callback(args)
def dbus_signal_async(
diff --git a/src/sdbus/dbus_proxy_sync_interface_base.py b/src/sdbus/dbus_proxy_sync_interface_base.py
index ca7e737..6358c74 100644
--- a/src/sdbus/dbus_proxy_sync_interface_base.py
+++ b/src/sdbus/dbus_proxy_sync_interface_base.py
@@ -19,68 +19,125 @@
# 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 itertools import chain
+from typing import TYPE_CHECKING
+from weakref import WeakKeyDictionary, WeakValueDictionary
from .dbus_common_elements import (
+ DbusClassMeta,
DbusInterfaceMetaCommon,
- DbusSomethingAsync,
- DbusSomethingSync,
+ DbusMemberAsync,
+ DbusMemberCommon,
+ DbusRemoteObjectMeta,
)
-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 collections.abc import Iterable, Iterator
+ from typing import Any, Optional
+
+ 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):
- def __new__(cls, name: str,
- bases: Tuple[type, ...],
- namespace: Dict[str, Any],
- interface_name: Optional[str] = None,
- 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."
+ @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}"
)
- if isinstance(value, DbusSomethingSync):
- value.interface_name = interface_name
- value.serving_enabled = serving_enabled
- declared_interfaces.add(key)
+ return all_python_dbus_attrs
- 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
+ @staticmethod
+ def _map_dbus_elements(
+ attr_name: str,
+ attr: Any,
+ meta: DbusClassMeta,
+ ) -> None:
+ if not isinstance(attr, DbusMemberCommon):
+ return
- super_declared_interfaces = set()
- for base in bases:
- if issubclass(base, DbusInterfaceBase):
- super_declared_interfaces.update(
- base._dbus_declared_interfaces)
+ if isinstance(attr, DbusMemberAsync):
+ raise TypeError(
+ f"Can't mix async methods in sync interface: {attr_name!r}"
+ )
- dbus_to_python_name_map.update(
- base._dbus_to_python_name_map
- )
+ 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],
+ interface_name: Optional[str] = None,
+ serving_enabled: bool = True,
+ ) -> DbusInterfaceMetaSync:
- for key in super_declared_interfaces & namespace.keys():
- raise TypeError("Attempted to overload dbus definition"
- " blocking interfaces do not support overloading")
+ 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_served_interfaces_names'] = \
- dbus_served_interfaces_names
- namespace['_dbus_declared_interfaces'] = declared_interfaces
- namespace['_dbus_to_python_name_map'] = dbus_to_python_name_map
+ 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)
new_cls = super().__new__(
cls, name, bases, namespace,
@@ -88,22 +145,35 @@ def __new__(cls, name: str,
serving_enabled,
)
- return cast(DbusInterfaceMetaSync, new_cls)
+ if interface_name is not None:
+ 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
+
+ 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_declared_interfaces: Set[str]
- _dbus_serving_enabled: bool
- _dbus_to_python_name_map: Dict[str, str]
- _dbus_served_interfaces_names: Set[str]
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)
+
+ @classmethod
+ def _dbus_iter_interfaces_meta(
+ cls,
+ ) -> Iterator[tuple[str, DbusClassMeta]]:
+
+ for base in reversed(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 7d9dc15..b14e73b 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, Literal
+
class DbusPeerInterface(
DbusInterfaceBase,
@@ -58,21 +61,23 @@ 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:
+ continue
- for interface_name in self._dbus_served_interfaces_names:
- dbus_properties_data = self._properties_get_all(
- interface_name)
+ 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 = meta.dbus_member_to_python_attr[member_name]
except KeyError:
if on_unknown_member == 'error':
raise
@@ -89,9 +94,9 @@ def properties_get_all_dict(
class DbusInterfaceCommon(
- DbusPeerInterface,
+ DbusPropertiesInterface,
DbusIntrospectable,
- DbusPropertiesInterface):
+ DbusPeerInterface):
...
@@ -102,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 277f781..0ba58e1 100644
--- a/src/sdbus/dbus_proxy_sync_method.py
+++ b/src/sdbus/dbus_proxy_sync_method.py
@@ -21,43 +21,32 @@
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,
+ DbusBoundSync,
+ DbusMemberSync,
DbusMethodCommon,
- DbusSomethingSync,
)
-from .sd_bus_internals import SdBus
-
-DEFAULT_BUS: Optional[SdBus] = None
-
-
-T_input = TypeVar('T_input')
-
if TYPE_CHECKING:
+ from collections.abc import Callable, Sequence
+ from typing import Any, Optional
+
from .dbus_proxy_sync_interface_base import DbusInterfaceBase
+T = TypeVar('T')
-class DbusMethodSync(DbusMethodCommon, DbusSomethingSync):
+
+class DbusMethodSync(DbusMethodCommon, DbusMemberSync):
def __get__(self,
obj: DbusInterfaceBase,
- obj_class: Optional[Type[DbusInterfaceBase]] = None,
+ 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):
@@ -67,19 +56,19 @@ 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,
- 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()
@@ -103,13 +92,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 +114,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 7da6e51..20e79a1 100644
--- a/src/sdbus/dbus_proxy_sync_property.py
+++ b/src/sdbus/dbus_proxy_sync_property.py
@@ -21,28 +21,22 @@
from inspect import iscoroutinefunction
from types import FunctionType
-from typing import (
- TYPE_CHECKING,
- Any,
- Callable,
- Generic,
- Optional,
- Type,
- TypeVar,
- cast,
-)
-
-from .dbus_common_elements import DbusPropertyCommon, DbusSomethingSync
-from .dbus_common_funcs import _check_sync_in_async_env
-
-T = TypeVar('T')
+from typing import TYPE_CHECKING, Generic, TypeVar, cast
+from .dbus_common_elements import DbusMemberSync, DbusPropertyCommon
+from .dbus_common_funcs import _check_sync_in_async_env
if TYPE_CHECKING:
+ from collections.abc import Callable
+ from typing import Any, Optional
+
from .dbus_proxy_sync_interface_base import DbusInterfaceBase
-class DbusPropertySync(DbusPropertyCommon, DbusSomethingSync, Generic[T]):
+T = TypeVar('T')
+
+
+class DbusPropertySync(DbusPropertyCommon, DbusMemberSync, Generic[T]):
def __init__(
self,
property_name: Optional[str],
@@ -69,25 +63,24 @@ 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. "
"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(
- 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:
@@ -98,25 +91,21 @@ def __set__(self, obj: DbusInterfaceBase, value: T) -> None:
)
if not self.property_signature:
- raise AttributeError('Dbus 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
- assert self.interface_name is not None
- new_call_message = obj._attached_bus. \
- new_property_set_message(
- obj._remote_service_name,
- obj._remote_object_path,
+ raise AttributeError('D-Bus property is read only')
+
+ 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/src/sdbus/default_bus.py b/src/sdbus/default_bus.py
new file mode 100644
index 0000000..ad10068
--- /dev/null
+++ b/src/sdbus/default_bus.py
@@ -0,0 +1,204 @@
+# 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 contextvars import ContextVar, Token
+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()
+bus_contextvar: ContextVar[SdBus] = ContextVar("DEFAULT_BUS")
+
+
+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 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(
+ "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 thread-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.
+ """
+ _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,
+ 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"""Asynchronously 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 received 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",
+ "set_context_default_bus",
+ "request_default_bus_name_async",
+ "request_default_bus_name",
+)
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/interface_generator.py b/src/sdbus/interface_generator.py
index 273f53c..12bb577 100644
--- a/src/sdbus/interface_generator.py
+++ b/src/sdbus/interface_generator.py
@@ -20,11 +20,17 @@
from __future__ import annotations
from pathlib import Path
-from typing import Dict, Iterable, Iterator, List, 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
+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
+ from xml.etree.ElementTree import Element
+
def _camel_case_to_snake_case_generator(camel: str) -> Iterator[str]:
i = iter(camel)
@@ -37,9 +43,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)
@@ -47,11 +56,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))
@@ -123,11 +136,11 @@ 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:
- accumulator: List[str] = [peek_str]
+ accumulator: list[str] = [peek_str]
round_braces_count = 0
curly_braces_count = 0
@@ -167,8 +180,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)
@@ -206,18 +219,18 @@ 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:])
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]
- 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}")
@@ -230,7 +243,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:
@@ -239,7 +252,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
@@ -254,15 +267,18 @@ 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)
+ def _can_use_unpivileged(self) -> bool:
+ return True
+
def _flags_iter(self) -> Iterator[str]:
if self.is_deprecated:
yield 'DbusDeprecatedFlag'
- if self.is_unpriveledged:
+ if not self.is_priveledged and self._can_use_unpivileged():
yield 'DbusUnprivilegedFlag'
@property
@@ -279,7 +295,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:
...
@@ -303,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):
@@ -331,7 +351,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}")
@@ -343,8 +363,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)
@@ -366,19 +386,19 @@ 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
- 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:
@@ -395,16 +415,25 @@ 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"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}")
class DbusPropertyIntrospection(DbusMemberAbstract):
- _EMITS_CHANGED_MAP: Dict[str, Optional[str]] = {
- 'true': 'DbusPropertyEmitsChangeFlag',
- 'false': None,
+ _EMITS_CHANGED_MAP: dict[
+ Union[bool, Literal['const', 'invalidates']], str
+ ] = {
+ True: 'DbusPropertyEmitsChangeFlag',
'invalidates': 'DbusPropertyEmitsInvalidationFlag',
'const': 'DbusPropertyConstFlag',
}
@@ -415,22 +444,29 @@ 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']] = True
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)
+ def _can_use_unpivileged(self) -> bool:
+ # 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]:
- 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 +476,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)
@@ -460,9 +502,12 @@ 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:
+ return False
+
def _parse_arg(self, arg: Element) -> None:
new_arg = DbusArgsIntrospection(arg)
@@ -473,13 +518,21 @@ 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:
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):
@@ -493,9 +546,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))
@@ -514,98 +567,258 @@ 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
+ @property
+ def has_members(self) -> bool:
+ return any((self.methods, self.properties, self.signals))
- env = JinjaEnv(trim_blocks=True)
- template = env.from_string(async_interface_template_txt)
- return template.render(interface=self)
+SKIP_INTERFACES = {
+ 'org.freedesktop.DBus.Properties',
+ 'org.freedesktop.DBus.Introspectable',
+ 'org.freedesktop.DBus.Peer',
+ 'org.freedesktop.DBus.ObjectManager',
+}
-async_import_header_txt = """
+INTERFACE_TEMPLATES: dict[str, str] = {
+ "generic_no_members": """\
+... # Interface has no members
+""",
+ "generic_method_flags": (
+ """\
+{% 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 %}
+{% if method.wants_rename %}
+method_name="{{method.method_name}}",
+{% endif %}
+"""
+ ),
+ "generic_property_flags": (
+ """\
+{% if a_property.dbus_signature %}
+property_signature="{{ a_property.dbus_signature }}",
+{% endif %}
+{% 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": """\
from __future__ import annotations
-from typing import Any, Dict, List, Tuple
+from typing import Any
-from sdbus import (DbusDeprecatedFlag, DbusInterfaceCommonAsync,
- DbusNoReplyFlag, DbusPropertyConstFlag,
- DbusPropertyEmitsChangeFlag,
- DbusPropertyEmitsInvalidationFlag, DbusPropertyExplicitFlag,
- DbusUnprivilegedFlag, dbus_method_async,
- dbus_property_async, dbus_signal_async)
-"""
+""",
+ "async_imports_header": """from sdbus import (
+ DbusDeprecatedFlag,
+ DbusInterfaceCommonAsync,
+ DbusNoReplyFlag,
+ DbusPropertyConstFlag,
+ DbusPropertyEmitsChangeFlag,
+ DbusPropertyEmitsInvalidationFlag,
+ DbusPropertyExplicitFlag,
+ DbusUnprivilegedFlag,
+ dbus_method_async,
+ dbus_property_async,
+ dbus_signal_async,
+)
+
+""",
+ "async_main": (
+ """\
+{% if include_import_header %}
+ {% include 'generic_header' %}
+
+ {% include 'async_imports_header' %}
+{% endif %}
-async_interface_template_txt = """
+{% for interface in interfaces %}
+ {% include 'async_interface' %}
+{% endfor %}
+"""
+ ),
+ "async_interface": (
+ """\
class {{ interface.python_name }}(
DbusInterfaceCommonAsync,
- interface_name='{{ interface.interface_name }}',
+ interface_name="{{ interface.interface_name }}",
):
-{% for method in interface.methods %}
+{% filter indent(first=True) %}
+ {% if interface.has_members %}
+ {% for method in interface.methods %}
+ {% include 'async_method' %}
- @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 }},
+ {% 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": (
+ """\
+@dbus_method_async(
+{% 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,
+)
+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
+ {{ arg_name }}: {{ arg_type }},
{% endfor %}
-{% for a_property in interface.properties %}
+) -> {{ method.result_typing }}:
+ raise NotImplementedError
- @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 %}
+"""
+ ),
+ "async_property": (
+ """\
+@dbus_property_async(
+{% filter indent(first=True) %}
+ {% include 'generic_property_flags' %}
+{% endfilter %}
+)
+def {{ a_property.python_name }}(self) -> {{ a_property.typing }}:
+ raise NotImplementedError
- @dbus_signal_async(
+"""
+ ),
+ "async_signal": (
+ """\
+@dbus_signal_async(
{% if signal.dbus_signature %}
- signal_signature='{{ 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 }},
+ flags={{ signal.flags_str }},
{% endif %}
- )
- def {{ signal.python_name }}(self) -> {{ signal.typing }}:
- raise NotImplementedError
+{% if signal.wants_rename %}
+ signal_name="{{signal.method_name}}",
+{% endif %}
+)
+def {{ signal.python_name }}(self) -> {{ signal.typing }}:
+ raise NotImplementedError
+
+"""
+ ),
+ "blocking_imports_header": """\
+from sdbus import (
+ DbusDeprecatedFlag,
+ DbusInterfaceCommon,
+ DbusNoReplyFlag,
+ DbusPropertyConstFlag,
+ DbusPropertyEmitsChangeFlag,
+ DbusPropertyEmitsInvalidationFlag,
+ DbusPropertyExplicitFlag,
+ DbusUnprivilegedFlag,
+ dbus_method,
+ dbus_property,
+)
+
+""",
+ "blocking_main": (
+ """\
+{% if include_import_header %}
+ {% include 'generic_header' %}
+
+ {% include 'blocking_imports_header' %}
+{% endif %}
+
+{% for interface in interfaces %}
+
+ {% include 'blocking_interface' %}
{% endfor %}
"""
+ ),
+ "blocking_interface": (
+ """\
+class {{ interface.python_name }}(
+ DbusInterfaceCommon,
+ interface_name="{{ interface.interface_name }}",
+):
+{% filter indent(first=True) %}
+ {% if interface.has_members %}
+ {% for method in interface.methods %}
+ {% include 'blocking_method' %}
-SKIP_INTERFACES = {
- 'org.freedesktop.DBus.Properties',
- 'org.freedesktop.DBus.Introspectable',
- 'org.freedesktop.DBus.Peer',
- 'org.freedesktop.DBus.ObjectManager',
+ {% endfor %}
+ {% for a_property in interface.properties %}
+ {% include 'blocking_property' %}
+
+ {% endfor %}
+ {% else %}
+ {% include 'generic_no_members' %}
+
+ {% endif %}
+{% endfilter %}
+"""
+ ),
+ "blocking_method": (
+ """\
+@dbus_method(
+{% 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 %}
+ {{ arg_name }}: {{ arg_type }},
+{% endfor %}
+) -> {{ method.result_typing }}:
+ raise NotImplementedError
+
+"""
+ ),
+ "blocking_property": (
+ """\
+@dbus_property(
+{% filter indent(first=True) %}
+ {% include 'generic_property_flags' %}
+{% endfilter %}
+)
+def {{ a_property.python_name }}(self) -> {{ a_property.typing }}:
+ raise NotImplementedError
+
+"""
+ ),
}
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}")
@@ -624,28 +837,38 @@ 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)
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:
- 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
+ template_name = "async_main" if do_async else "blocking_main"
+
+ 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/src/sdbus/sd_bus_internals.c b/src/sdbus/sd_bus_internals.c
index c7c887a..b97f444 100644
--- a/src/sdbus/sd_bus_internals.c
+++ b/src/sdbus/sd_bus_internals.c
@@ -21,26 +21,32 @@
#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;
// 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* add_writer_str = NULL;
+PyObject* remove_writer_str = NULL;
PyObject* empty_str = NULL;
PyObject* null_str = NULL;
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
@@ -50,6 +56,18 @@ static void SdBusSlot_dealloc(SdBusSlotObject* self) {
SD_BUS_DEALLOC_TAIL;
}
+static PyObject* SdBusSlot_close(SdBusSlotObject* self, PyObject* Py_UNUSED(args)) {
+ 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),
@@ -59,12 +77,13 @@ PyType_Spec SdBusSlotType = {
(PyType_Slot[]){
{Py_tp_new, PyType_GenericNew},
{Py_tp_dealloc, (destructor)SdBusSlot_dealloc},
+ {Py_tp_methods, SdBusSlot_methods},
{0, NULL},
},
};
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;
@@ -123,19 +142,39 @@ 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"));
- 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"));
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"));
@@ -154,6 +193,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.h b/src/sdbus/sd_bus_internals.h
index a928e2b..71784b3 100644
--- a/src/sdbus/sd_bus_internals.h
+++ b/src/sdbus/sd_bus_internals.h
@@ -239,26 +239,32 @@
#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;
// 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* add_writer_str;
+extern PyObject* remove_writer_str;
extern PyObject* empty_str;
extern PyObject* null_str;
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) {
@@ -326,7 +332,11 @@ extern PyObject* SdBusMessage_class;
typedef struct {
PyObject_HEAD;
sd_bus* sd_bus_ref;
- PyObject* reader_fd;
+ 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.py b/src/sdbus/sd_bus_internals.py
index c3d9a26..2981b17 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
@@ -19,27 +19,20 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
from __future__ import annotations
-from asyncio import Future, Queue
-from typing import (
- Any,
- Callable,
- Coroutine,
- Dict,
- List,
- Optional,
- Sequence,
- Tuple,
- Type,
- Union,
-)
+from asyncio import Future
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from collections.abc import Callable, 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]
-DbusCompleteTypes = Union[DbusBasicTypes, DbusStructType,
- DbusDictType, DbusVariantType, DbusListType]
+ 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 '
@@ -50,16 +43,18 @@
class SdBusSlot:
"""Holds reference to SdBus slot"""
- ...
+
+ def close(self) -> None:
+ raise NotImplementedError(__STUB_ERROR)
class SdBusInterface:
- 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,
@@ -67,7 +62,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)
@@ -90,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:
@@ -116,7 +114,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:
@@ -131,6 +129,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
@@ -148,7 +149,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:
@@ -187,12 +188,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]:
@@ -217,6 +218,7 @@ def start(self) -> None:
raise NotImplementedError(__STUB_ERROR)
address: Optional[str] = None
+ method_call_timeout_usec: int = 0
def sd_bus_open() -> SdBus:
@@ -251,12 +253,12 @@ 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
-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
@@ -288,9 +290,25 @@ class SdBusLibraryError(SdBusBaseError):
...
-DBUS_ERROR_TO_EXCEPTION: Dict[str, Exception] = {}
+class SdBusRequestNameError(SdBusBaseError):
+ ...
-EXCEPTION_TO_DBUS_ERROR: Dict[Exception, str] = {}
+
+class SdBusRequestNameInQueueError(SdBusRequestNameError):
+ ...
+
+
+class SdBusRequestNameExistsError(SdBusRequestNameError):
+ ...
+
+
+class SdBusRequestNameAlreadyOwnerError(SdBusRequestNameError):
+ ...
+
+
+DBUS_ERROR_TO_EXCEPTION: dict[str, type[Exception]] = {}
+
+EXCEPTION_TO_DBUS_ERROR: dict[type[Exception], str] = {}
DbusDeprecatedFlag: int = 0
DbusHiddenFlag: int = 0
@@ -301,3 +319,7 @@ class SdBusLibraryError(SdBusBaseError):
DbusPropertyEmitsInvalidationFlag: int = 0
DbusPropertyExplicitFlag: int = 0
DbusSensitiveFlag: int = 0
+
+NameAllowReplacementFlag: int = 0
+NameReplaceExistingFlag: int = 0
+NameQueueFlag: int = 0
diff --git a/src/sdbus/sd_bus_internals_bus.c b/src/sdbus/sd_bus_internals_bus.c
index fb920b4..ca7b817 100644
--- a/src/sdbus/sd_bus_internals_bus.c
+++ b/src/sdbus/sd_bus_internals_bus.c
@@ -19,11 +19,24 @@
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#include
+#include
+#include
+#include
#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));
+ }
+ if (NULL != self->timer_fd) {
+ Py_XDECREF(PyObject_CallMethodObjArgs(self->loop, remove_reader_str, self->timer_fd, NULL));
+ Py_DECREF(self->timer_fd);
+ close(self->timer_fd_int);
+ }
sd_bus_unref(self->sd_bus_ref);
- Py_XDECREF(self->reader_fd);
+ Py_XDECREF(self->bus_fd);
+ Py_XDECREF(self->loop);
SD_BUS_DEALLOC_TAIL;
}
@@ -155,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));
@@ -229,46 +230,31 @@ 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* _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_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 +266,7 @@ static PyObject* SdBus_drive(SdBusObject* self, PyObject* Py_UNUSED(args)) {
return NULL;
}
}
+ CHECK_ASYNCIO_WATCHERS;
Py_RETURN_NONE;
}
@@ -291,7 +278,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;
}
@@ -317,18 +304,11 @@ 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
- PyObject* running_loop CLEANUP_PY_OBJECT = CALL_PYTHON_AND_CHECK(PyObject_CallFunctionObjArgs(asyncio_get_running_loop, NULL));
+ 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", ""));
@@ -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;
}
@@ -376,17 +356,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 +373,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,66 +397,84 @@ 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* 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", ""));
- // 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,
_SdBus_match_signal_instant_callback, new_future));
- CHECK_SD_BUS_READER;
+ CHECK_ASYNCIO_WATCHERS;
Py_INCREF(new_future);
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) {
- // A bit unpythonic but SdBus_drive does not error out
+ // A bit unpythonic but SdBus_process does not error out
return 0;
}
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) {
@@ -512,17 +503,15 @@ 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));
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;
- }
- CHECK_SD_BUS_READER;
+ CALL_PYTHON_INT_CHECK(PyObject_SetAttrString(new_future, "_sd_bus_py_slot", (PyObject*)new_slot_object));
+ CHECK_ASYNCIO_WATCHERS;
return new_future;
}
@@ -544,8 +533,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
@@ -601,6 +607,14 @@ 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)));
+ }
+ if (NULL != self->timer_fd) {
+ Py_XDECREF(PyObject_CallMethodObjArgs(self->loop, remove_reader_str, self->timer_fd, NULL));
+ // TODO: Close timerfd
+ }
Py_RETURN_NONE;
}
@@ -609,28 +623,99 @@ 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 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
+ Py_RETURN_NONE;
+ } else {
+ self->asyncio_watchers_last_state = events_to_watch;
+ }
+
+ 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, "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"},
- {"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"},
- {"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"},
+ {"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.")},
+ {"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.")},
+ {"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.")},
+ {"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},
};
@@ -646,8 +731,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, "Bus address", NULL},
+ {"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/src/sdbus/sd_bus_internals_funcs.c b/src/sdbus/sd_bus_internals_funcs.c
index 0dbee31..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;
}
@@ -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..df31e7c 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);
@@ -320,11 +323,19 @@ 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, "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.")},
+ {"_stop_export", (PyCFunction)SdBusInterface_stop_export, METH_NOARGS, PyDoc_STR("Stop exporting object.")},
{NULL, NULL, 0, NULL},
};
diff --git a/src/sdbus/sd_bus_internals_message.c b/src/sdbus/sd_bus_internals_message.c
index d6f7652..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) {
@@ -577,7 +578,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;
}
@@ -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;
}
@@ -966,6 +971,7 @@ static PyObject* SdBusMessage_get_contents2(SdBusMessageObject* self, PyObject*
Py_RETURN_NONE;
}
+ 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,
@@ -979,6 +985,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);
@@ -1003,17 +1032,19 @@ 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.")},
+ {"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.")},
+ {"send", (PyCFunction)SdBusMessage_send, METH_NOARGS, PyDoc_STR("Queue message to be sent.")},
{NULL, NULL, 0, NULL},
};
@@ -1083,12 +1114,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},
};
diff --git a/src/sdbus/unittest.py b/src/sdbus/unittest.py
index 4230f0e..62ac280 100644
--- a/src/sdbus/unittest.py
+++ b/src/sdbus/unittest.py
@@ -19,16 +19,36 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
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
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 weakref import ref as weak_ref
+
+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:
+ from collections.abc import Iterator
+ from contextlib import AbstractAsyncContextManager
+ from typing import Any, Optional, TypeVar, Union
+
+ from .dbus_proxy_async_signal import (
+ DbusBoundSignalAsyncBase,
+ DbusSignalAsync,
+ )
+ from .sd_bus_internals import SdBus, SdBusSlot
+
+ T = TypeVar('T')
-from sdbus import sd_bus_open_user, set_default_bus
dbus_config = '''
@@ -45,49 +65,178 @@
'''
-class IsolatedDbusTestCase(IsolatedAsyncioTestCase):
- dbus_executable_name: ClassVar[str] = 'dbus-daemon'
+class DbusSignalRecorderBase:
+ def __init__(
+ self,
+ timeout: Union[int, float],
+ ):
+ 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()
+
+ @property
+ def output(self) -> list[Any]:
+ return self._captured_data.copy()
+
+
+class DbusSignalRecorderRemote(DbusSignalRecorderBase):
+ def __init__(
+ self,
+ timeout: Union[int, float],
+ bus: SdBus,
+ remote_signal: DbusProxySignalAsync[Any],
+ ):
+ super().__init__(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,
+ )
- def setUp(self) -> None:
- self.temp_dir = TemporaryDirectory()
- self.temp_dir_path = Path(self.temp_dir.name)
+ 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,
+ timeout: Union[int, float],
+ local_signal: DbusLocalSignalAsync[Any],
+ ):
+ super().__init__(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()
- self.dbus_socket_path = self.temp_dir_path / 'test_dbus.socket'
- self.pid_path = self.temp_dir_path / 'dbus.pid'
+ if local_signal is None:
+ raise RuntimeError
- self.dbus_config_file = self.temp_dir_path / 'dbus.config'
+ local_signal.local_callbacks.add(self._callback_method)
+ return self
- 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))
+
+@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-")
+ )
+ )
+
+ 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}"
+
+ old_bus = _get_defaul_bus_tls()
+ bus = sd_bus_open_user()
+ _set_default_bus_tls(bus)
+ exit_stack.callback(_set_default_bus_tls, old_bus)
+ yield bus
- self.old_session_bus_address = environ.get('DBUS_SESSION_BUS_ADDRESS')
- environ[
- 'DBUS_SESSION_BUS_ADDRESS'] = f"unix:path={self.dbus_socket_path}"
-
- self.bus = sd_bus_open_user()
- set_default_bus(self.bus)
- async def asyncSetUp(self) -> None:
- set_default_bus(self.bus)
-
- def tearDown(self) -> None:
- with open(self.pid_path) as pid_file:
- dbus_pid = int(pid_file.read())
-
- 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
+class IsolatedDbusTestCase(IsolatedAsyncioTestCase):
+ 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)
+
+ def assertDbusSignalEmits(
+ self,
+ signal: DbusBoundSignalAsyncBase[Any],
+ timeout: Union[int, float] = 1,
+ ) -> AbstractAsyncContextManager[DbusSignalRecorderBase]:
+
+ if isinstance(signal, DbusLocalSignalAsync):
+ return DbusSignalRecorderLocal(timeout, signal)
+ elif isinstance(signal, DbusProxySignalAsync):
+ return DbusSignalRecorderRemote(timeout, self.bus, signal)
+ else:
+ raise TypeError("Unknown or unsupported signal class.")
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/inspect.py b/src/sdbus/utils/inspect.py
new file mode 100644
index 0000000..5306db7
--- /dev/null
+++ b/src/sdbus/utils/inspect.py
@@ -0,0 +1,141 @@
+# 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_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
+
+ 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:
+ """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()
+
+ 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}")
+
+
+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/src/sdbus/utils/parse.py b/src/sdbus/utils/parse.py
new file mode 100644
index 0000000..f56e5fe
--- /dev/null
+++ b/src/sdbus/utils/parse.py
@@ -0,0 +1,409 @@
+# 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 ..dbus_common_funcs import _parse_properties_vardict
+from ..dbus_proxy_async_interface_base import (
+ DBUS_CLASS_TO_META,
+ DBUS_INTERFACE_NAME_TO_CLASS,
+ DbusInterfaceBaseAsync,
+)
+from ..dbus_proxy_sync_interface_base import DbusInterfaceBase
+
+if TYPE_CHECKING:
+ from collections.abc import Iterable
+ from typing import Any, Literal, Optional, Union
+
+ from ..dbus_proxy_async_interfaces import DBUS_PROPERTIES_CHANGED_TYPING
+
+ InterfacesBaseClasses = Union[DbusInterfaceBaseAsync, DbusInterfaceBase]
+ InterfacesBaseTypes = type[InterfacesBaseClasses]
+ InterfacesInputElements = Union[InterfacesBaseClasses, InterfacesBaseTypes]
+ InterfacesInput = Union[
+ InterfacesInputElements,
+ Iterable[InterfacesInputElements],
+ ]
+ InterfacesToClassMap = dict[
+ frozenset[str],
+ InterfacesBaseTypes,
+ ]
+ OnUnknownMember = Literal['error', 'ignore', 'reuse']
+ OnUnknownInterface = Literal['error', 'none']
+ ParseGetManaged = dict[
+ str,
+ 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,
+ 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
+ )
+
+ meta = DBUS_CLASS_TO_META[DBUS_INTERFACE_NAME_TO_CLASS[interface_name]]
+
+ for invalidated_property in invalidated_properties:
+ changed_properties[invalidated_property] = ('0', None)
+
+ return _parse_properties_vardict(
+ meta.dbus_member_to_python_attr,
+ properties_changed_data[1],
+ on_unknown_member,
+ )
+
+
+SKIP_INTERFACES = frozenset((
+ 'org.freedesktop.DBus.Properties',
+ 'org.freedesktop.DBus.Introspectable',
+ 'org.freedesktop.DBus.Peer',
+ 'org.freedesktop.DBus.ObjectManager',
+))
+
+
+def _create_interfaces_map(
+ interfaces: tuple[InterfacesBaseTypes, ...],
+) -> InterfacesToClassMap:
+
+ interfaces_to_class_map: InterfacesToClassMap = {}
+
+ for interface in interfaces:
+ 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
+
+
+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:
+ if raise_key_error:
+ raise
+
+ return None
+
+
+def _get_member_map_from_class(
+ python_class: Optional[InterfacesBaseTypes],
+) -> 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: InterfacesInput,
+ 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.
+
+ 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.
+ :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.
+ """
+ interfaces_types = _interfaces_input_to_types(interfaces)
+ interfaces_to_class_map = _create_interfaces_map(interfaces_types)
+
+ path, properties_data = interfaces_added_data
+
+ python_class = (
+ _get_class_from_interfaces(
+ 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)
+ python_properties: dict[str, Any] = {}
+ 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(
+ interface_member_map,
+ properties,
+ on_unknown_member,
+ )
+ )
+
+ return (
+ path,
+ python_class,
+ _translate_and_merge_members(
+ properties_data,
+ dbus_to_python_member_map,
+ on_unknown_member,
+ ),
+ )
+
+
+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.
+
+ 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.
+ :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``).
+ """
+ interfaces_types = _interfaces_input_to_types(interfaces)
+ interfaces_to_class_map = _create_interfaces_map(interfaces_types)
+
+ path, interfaces_removed = interfaces_removed_data
+
+ python_class = (
+ _get_class_from_interfaces(
+ interfaces_to_class_map,
+ interfaces_removed,
+ on_unknown_interface == "error",
+ use_interface_subsets,
+ )
+ )
+
+ 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',
+ *,
+ use_interface_subsets: bool = False,
+) -> 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.
+ :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.
+
+ *New in version 0.12.0.*
+ """
+ interfaces_types = _interfaces_input_to_types(interfaces)
+ interfaces_to_class_map = _create_interfaces_map(interfaces_types)
+
+ 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",
+ use_interface_subsets,
+ )
+ )
+ 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/src/sdbus_async/dbus_daemon/__init__.py b/src/sdbus_async/dbus_daemon/__init__.py
index 0816d03..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,
@@ -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(
@@ -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,8 +134,8 @@ async def start_service_by_name(
raise NotImplementedError
@dbus_property_async('as')
- def features(self) -> List[str]:
- """List of dbus daemon features.
+ def features(self) -> list[str]:
+ """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"""
+ def interfaces(self) -> list[str]:
+ """Extra D-Bus daemon interfaces"""
raise NotImplementedError
@dbus_signal_async('s')
@@ -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 39ca578..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
@@ -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',
@@ -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,8 +125,8 @@ def start_service_by_name(
raise NotImplementedError
@dbus_property('as')
- def features(self) -> List[str]:
- """List of dbus daemon features.
+ def features(self) -> list[str]:
+ """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"""
+ def interfaces(self) -> list[str]:
+ """Extra D-Bus daemon interfaces"""
raise NotImplementedError
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()
diff --git a/test/benchmarks/bench_block_ping.py b/test/benchmarks/bench_block_ping.py
new file mode 100644
index 0000000..36c68d6
--- /dev/null
+++ b/test/benchmarks/bench_block_ping.py
@@ -0,0 +1,52 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+
+# Copyright (C) 2025 igo95862
+
+# This file is part of python-sdbus
+
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+from __future__ import annotations
+
+from time import perf_counter
+
+import pyperf # type: ignore
+from sdbus.unittest import _isolated_dbus
+
+from sdbus import DbusInterfaceCommon
+
+
+def bench_block_ping(loops: int) -> float:
+ with _isolated_dbus() as bus:
+ dbus_interface = DbusInterfaceCommon(
+ "org.freedesktop.DBus",
+ "/org/freedesktop/DBus",
+ bus,
+ )
+
+ start = perf_counter()
+
+ for _ in range(loops):
+ dbus_interface.dbus_ping()
+
+ return perf_counter() - start
+
+
+def main() -> None:
+ runner = pyperf.Runner()
+ runner.bench_time_func('sdbus_block_ping', bench_block_ping)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/test/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()
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/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/
diff --git a/test/leak_tests.py b/test/leak_tests.py
index c9a7037..59dcf0c 100644
--- a/test/leak_tests.py
+++ b/test/leak_tests.py
@@ -29,19 +29,20 @@
)
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
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,
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'
@@ -115,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)
@@ -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_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()
diff --git a/test/test_deprecations.py b/test/test_deprecations.py
new file mode 100644
index 0000000..aec99ee
--- /dev/null
+++ b/test/test_deprecations.py
@@ -0,0 +1,25 @@
+# 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
+
+if __name__ == '__main__':
+ main()
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_interface_generator.py b/test/test_interface_generator.py
index f208477..5d072c0 100644
--- a/test/test_interface_generator.py
+++ b/test/test_interface_generator.py
@@ -21,14 +21,17 @@
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,
- generate_async_py_file,
+ generate_py_file,
interface_name_to_class,
interfaces_from_str,
)
+from sdbus.unittest import IsolatedDbusTestCase
test_xml = """
+
+
+
+
+
+
+
+
+
@@ -62,10 +77,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(
@@ -84,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'):
@@ -96,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(
@@ -141,7 +169,174 @@ 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,
+ True,
+ )
+ 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_py_file(interfaces_intro)
+ self.assertIn('flags=DbusPropertyEmitsInvalidationFlag', generated)
+ self.assertIn('flags=DbusPropertyConstFlag', generated)
+
+
+class TestGeneratorAgainstDbus(IsolatedDbusTestCase):
+ 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(
+ [
+ "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,
+ )
+ self.assertIn(
+ "async",
+ generated_interface,
+ )
+
+ def test_generate_from_connection_blocking(self) -> None:
+ 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,
+ )
+
+
+INTERFACE_NO_MEMBERS_XML = """
+
+
+
+
+
+
+
+"""
+
+
+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),
+ 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__":
diff --git a/test/test_low_level_api.py b/test/test_low_level_api.py
index 62b7f33..1030bb3 100644
--- a/test/test_low_level_api.py
+++ b/test/test_low_level_api.py
@@ -19,7 +19,8 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
from __future__ import annotations
-from unittest import SkipTest, main
+from asyncio import get_running_loop
+from unittest import SkipTest, TestCase, main
from sdbus.sd_bus_internals import (
SdBus,
@@ -31,13 +32,25 @@
from sdbus.unittest import IsolatedDbusTestCase
-class TestDbusTypes(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:
try:
self.assertTrue(
@@ -73,12 +86,25 @@ 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:
+ 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()
diff --git a/test/test_low_level_errors.py b/test/test_low_level_errors.py
index 6f6e48c..167a434 100644
--- a/test/test_low_level_errors.py
+++ b/test/test_low_level_errors.py
@@ -22,10 +22,11 @@
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
from sdbus import (
- DbusFailedError,
DbusInterfaceCommonAsync,
dbus_method_async,
dbus_property_async,
@@ -48,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:
@@ -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_object_manager.py b/test/test_object_manager.py
index 2e881cd..2c01a9a 100644
--- a/test/test_object_manager.py
+++ b/test/test_object_manager.py
@@ -17,13 +17,15 @@
# 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.exceptions import DbusUnknownObjectError
from sdbus.unittest import IsolatedDbusTestCase
+from sdbus.utils import (
+ parse_get_managed_objects,
+ parse_interfaces_added,
+ parse_interfaces_removed,
+)
from sdbus import (
DbusInterfaceCommonAsync,
@@ -38,7 +40,7 @@
class ObjectManagerTestInterface(
DbusObjectManagerInterfaceAsync,
- interface_name='org.test.test',
+ interface_name='org.test.objectmanager',
):
@dbus_method_async(
result_signature='s',
@@ -68,7 +70,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()
@@ -81,31 +82,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
@@ -118,10 +102,15 @@ async def catch_interfaces_removed() -> Tuple[str, List[str]]:
TEST_NUMBER,
)
- object_manager.remove_managed_object(managed_object)
+ with self.subTest("Test interfaces added parser"):
+ parse_interfaces_added(ManagedInterface, caught_added)
+
+ 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)
@@ -138,3 +127,282 @@ def test_expot_with_no_manager(self) -> None:
MANAGED_PATH,
managed_object,
)
+
+ async def test_parse_interfaces_added_removed(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'
+
+ 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)
+
+ managed_object = ManagedTwoInterface()
+
+ async with self.assertDbusSignalEmits(
+ object_manager_connection.interfaces_added
+ ) as added_interfaces_catch:
+ object_manager.export_with_manager(MANAGED_PATH, managed_object)
+
+ caught_added = added_interfaces_catch.output[0]
+
+ with self.subTest('Parse added 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 added 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 added 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 added 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)
+
+ 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)
+
+ async with self.assertDbusSignalEmits(
+ object_manager_connection.interfaces_removed
+ ) as removed_interfaces_catch:
+ object_manager.remove_managed_object(managed_object)
+
+ interfaces_removed_data = removed_interfaces_catch.output[0]
+
+ 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)
+
+ 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)
+
+ removed_path, removed_interfaces = removed.output[0]
+ self.assertEqual(removed_path, MANAGED_PATH)
+ self.assertIn(MANAGED_INTERFACE_NAME, removed_interfaces)
+
+ with self.assertRaises(DbusUnknownObjectError):
+ await managed_proxy.test_int
diff --git a/test/test_read_write_dbus_types.py b/test/test_read_write_dbus_types.py
index d3a1a01..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()
@@ -429,6 +428,17 @@ 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):
+ self.assertEqual(
+ message.get_contents(),
+ "test",
+ )
+
if __name__ == "__main__":
main()
diff --git a/test/test_request_name.py b/test/test_request_name.py
new file mode 100644
index 0000000..e221831
--- /dev/null
+++ b/test/test_request_name.py
@@ -0,0 +1,227 @@
+# 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 asyncio import get_running_loop, sleep, wait_for
+from unittest import main
+
+from sdbus.exceptions import (
+ SdBusLibraryError,
+ SdBusRequestNameAlreadyOwnerError,
+ SdBusRequestNameError,
+ SdBusRequestNameExistsError,
+ SdBusRequestNameInQueueError,
+)
+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,
+ 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 TestRequestNameLowLevel(IsolatedDbusTestCase):
+ 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,
+ )
+ )
+
+ 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)
+
+
+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,
+ )
+
+ 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)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/test_sd_bus_async.py b/test/test_sdbus_async.py
similarity index 57%
rename from test/test_sd_bus_async.py
rename to test/test_sdbus_async.py
index 8cd5699..4ce7723 100644
--- a/test/test_sd_bus_async.py
+++ b/test/test_sdbus_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
@@ -17,32 +17,34 @@
# 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
+from asyncio import run as asyncio_run
+from asyncio import sleep, wait_for
from asyncio.subprocess import create_subprocess_exec
-from typing import Tuple
+from typing import TYPE_CHECKING
from unittest import SkipTest
-from sdbus.dbus_common_funcs import PROPERTY_FLAGS_MASK, count_bits
+from sdbus.exceptions import (
+ DbusFailedError,
+ DbusFileExistsError,
+ DbusNoReplyError,
+ DbusPropertyReadOnlyError,
+ DbusUnknownObjectError,
+ SdBusLibraryError,
+ SdBusUnmappedMessageError,
+)
from sdbus.sd_bus_internals import (
DBUS_ERROR_TO_EXCEPTION,
- DbusDeprecatedFlag,
- DbusPropertyConstFlag,
DbusPropertyEmitsChangeFlag,
- is_interface_name_valid,
)
from sdbus.unittest import IsolatedDbusTestCase
+from sdbus.utils.parse import parse_properties_changed
from sdbus import (
- DbusFailedError,
- DbusFileExistsError,
DbusInterfaceCommonAsync,
DbusNoReplyFlag,
- DbusUnknownObjectError,
- SdBusLibraryError,
- SdBusUnmappedMessageError,
dbus_method_async,
dbus_method_async_override,
dbus_property_async,
@@ -51,6 +53,13 @@
get_current_message,
)
+if TYPE_CHECKING:
+ from sdbus.dbus_proxy_async_interfaces import (
+ DBUS_PROPERTIES_CHANGED_TYPING,
+ )
+else:
+ DBUS_PROPERTIES_CHANGED_TYPING = None
+
class TestPing(IsolatedDbusTestCase):
@@ -83,15 +92,20 @@ 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__()
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")
@@ -128,6 +142,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,
@@ -149,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
@@ -174,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()
@@ -185,19 +211,42 @@ 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()
async def looong_method(self) -> None:
await sleep(100)
+ @dbus_signal_async()
+ def empty_signal(self) -> None:
+ raise NotImplementedError
+
+ @dbus_method_async()
+ 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
+
+ @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'
@@ -210,7 +259,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('/')
@@ -289,6 +338,26 @@ 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]
+ )
+
+ 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()
@@ -305,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()
@@ -327,19 +395,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(
- test_object.properties_changed.dbus_signal.signal_name,
- test_object._dbus_to_python_name_map,
+ "TestInt",
+ dbus_elements_map[TEST_INTERFACE_NAME],
)
self.assertIn(
- test_subclass.properties_changed.dbus_signal.signal_name,
- test_subclass._dbus_to_python_name_map,
+ "TestInt",
+ dbus_elements_map[TEST_INTERFACE_NAME],
)
self.assertIn(
- test_subclass.test_property.dbus_property.property_name,
- test_subclass._dbus_to_python_name_map,
+ "TestProperty",
+ dbus_elements_map[TEST_INTERFACE_NAME],
)
with self.subTest('Tripple subclass'):
@@ -367,18 +442,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()
@@ -420,19 +483,24 @@ 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')
- ai_dbus = test_object_connection.test_signal.__aiter__()
- aw_dbus = ai_dbus.__anext__()
- q = test_object.test_signal._get_local_queue()
-
- loop.call_at(0, 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)
- self.assertEqual(test_tuple, await wait_for(aw_dbus, 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, await wait_for(q.get(), timeout=1))
+ 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()
@@ -443,7 +511,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
@@ -464,7 +532,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
@@ -485,7 +553,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
@@ -500,6 +568,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()
@@ -617,51 +712,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()
@@ -673,18 +723,39 @@ 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_bus_timerfd(self) -> None:
test_object, test_object_connection = initialize_object()
- message_queue = await self.bus.get_signal_queue_async(
+ 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.assertLess(loop.time() - start, 0.2)
+
+ async def test_signal_queue_wildcard_match(self) -> None:
+ test_object, test_object_connection = initialize_object()
+
+ 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,
- test_object.test_signal.dbus_signal.signal_name)
+ 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
@@ -709,96 +780,289 @@ 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')
+ async def test_properties_get_all_dict(self) -> None:
+ test_object, test_object_connection = initialize_object()
- 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',
- ):
- ...
+ dbus_dict = await test_object_connection._properties_get_all(
+ TEST_INTERFACE_NAME)
- self.assertRaisesRegex(
- AssertionError,
- '^Invalid interface name',
- test_bad_interface_name,
+ self.assertEqual(
+ await test_object.test_property,
+ dbus_dict['TestProperty'][1],
)
- 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,
+ self.assertEqual(
+ await test_object.test_property,
+ (
+ await test_object_connection.properties_get_all_dict()
+ )['test_property'],
)
- 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'
+ async def test_empty_signal(self) -> None:
+ test_object, test_object_connection = initialize_object()
+
+ 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.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()
+
+ test_str = 'should_be_emited'
- self.assertRaisesRegex(
- AssertionError,
- '^Invalid property name',
- test_bad_property_name,
+ async with self.assertDbusSignalEmits(
+ test_object_connection.properties_changed
+ ) as properties_changed_catch:
+ await test_object_connection.test_property.set_async(test_str)
+
+ properties_changed_data = properties_changed_catch.output[0]
+
+ parsed_dict_from_class = parse_properties_changed(
+ TestInterface, properties_changed_data)
+ self.assertEqual(
+ test_str,
+ parsed_dict_from_class['test_property'],
)
- 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
+ parsed_dict_from_object = parse_properties_changed(
+ test_object_connection, properties_changed_data)
+ self.assertEqual(
+ test_str,
+ parsed_dict_from_object['test_property'],
+ )
- self.assertRaisesRegex(
- AssertionError,
- '^Invalid signal name',
- test_bad_signal_name,
+ 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'])
- async def test_properties_get_all_dict(self) -> None:
+ async def test_property_private_setter(self) -> None:
test_object, test_object_connection = initialize_object()
- dbus_dict = await test_object_connection._properties_get_all(
- 'org.test.test')
+ 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)
+
+ async with self.assertDbusSignalEmits(
+ test_object_connection.properties_changed
+ ) as properties_changed_catch:
+ await test_object.test_property_private.set_async(new_value)
+
+ changed_properties = properties_changed_catch.output[0]
self.assertEqual(
- await test_object.test_property,
- dbus_dict['TestProperty'][1],
+ await test_object_connection.test_property_private,
+ new_value
+ )
+
+ 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.test_property,
- (
- await test_object_connection.properties_get_all_dict()
- )['test_property'],
+ 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,
)
+
+ 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(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()
+
+ 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()
+
+ 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()
+
+ 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
+
+ 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())
+
+ 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()
+
+ 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_async_bad_class.py b/test/test_sdbus_async_bad_class.py
new file mode 100644
index 0000000..92e2ee0
--- /dev/null
+++ b/test/test_sdbus_async_bad_class.py
@@ -0,0 +1,250 @@
+# 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 gc import collect
+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_asserts, skip_if_no_name_validations
+
+
+class TestInterface(
+ DbusInterfaceCommonAsync,
+ interface_name="org.example.good",
+):
+ @dbus_method_async(result_signature="i")
+ async def test_int(self) -> int:
+ return 1
+
+
+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
+
+ 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.invalidprop"
+ ):
+ @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.validprop"
+ ):
+ @dbus_property_async(
+ "s",
+ flags=DbusDeprecatedFlag | DbusPropertyEmitsChangeFlag,
+ )
+ def test_constant(self) -> str:
+ return "a"
+
+ def test_bad_subclass(self) -> None:
+ with self.assertRaises(ValueError):
+
+ class TestInheritence(TestInterface):
+ async def test_int(self) -> int:
+ return 2
+
+ with self.assertRaises(ValueError):
+
+ class TestInheritence2(TestInterface):
+ @dbus_method_async_override()
+ 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:
+ ...
+
+ 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
new file mode 100644
index 0000000..fe17b7e
--- /dev/null
+++ b/test/test_sdbus_async_introspection.py
@@ -0,0 +1,139 @@
+# 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 sdbus.unittest import IsolatedDbusTestCase
+
+from sdbus import DbusInterfaceCommonAsync, dbus_method_async
+
+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.intro1",
+ ):
+ @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.intro2",
+ ):
+ @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.intro3",
+ ):
+ @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.intro4",
+ ):
+ @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)
diff --git a/test/test_sd_bus_sync.py b/test/test_sdbus_block.py
similarity index 58%
rename from test/test_sd_bus_sync.py
rename to test/test_sdbus_block.py
index 724e80e..ef650d3 100644
--- a/test/test_sd_bus_sync.py
+++ b/test/test_sdbus_block.py
@@ -17,15 +17,15 @@
# 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 DbusPropertyReadOnlyError
from sdbus.unittest import IsolatedDbusTestCase
from sdbus_block.dbus_daemon import FreedesktopDbus
-from sdbus import DbusPropertyReadOnlyError
+from sdbus import DbusInterfaceCommon, dbus_method
class TestSync(IsolatedDbusTestCase):
@@ -46,17 +46,19 @@ 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'],
+ 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'):
@@ -73,6 +75,40 @@ 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(TwoInterface, OneInterface):
+ ...
+
+ test_combined = CombinedInterface("org.test", "/")
+ test_combined_interfaces = [
+ iface for iface, _ in test_combined._dbus_iter_interfaces_meta()
+ ]
+
+ # Verify the order of reported interfaces on the combined class.
+ self.assertEqual(test_combined_interfaces, [
+ "org.freedesktop.DBus.Peer",
+ "org.freedesktop.DBus.Introspectable",
+ "org.freedesktop.DBus.Properties",
+ "org.example.one",
+ "org.example.two",
+ ])
+
if __name__ == '__main__':
main()
diff --git a/test/test_sdbus_block_bad_class.py b/test/test_sdbus_block_bad_class.py
new file mode 100644
index 0000000..14855e7
--- /dev/null
+++ b/test/test_sdbus_block_bad_class.py
@@ -0,0 +1,203 @@
+# 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 gc import collect
+from unittest import TestCase
+from unittest import main as unittest_main
+
+from sdbus import DbusInterfaceCommon, dbus_method, dbus_property
+
+from .common_test_util import skip_if_no_name_validations
+
+
+class GoodDbusInterface(
+ DbusInterfaceCommon,
+ interface_name="org.example.test",
+):
+ @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(ValueError):
+
+ class BadMethodOverrideClass(GoodDbusInterface):
+ def test_method(self) -> None:
+ return
+
+ with self.subTest("D-Bus method override"), self.assertRaises(
+ ValueError
+ ):
+
+ class BadDbusMethodOverrideClass(GoodDbusInterface):
+ @dbus_method()
+ def test_method(self) -> None:
+ return
+
+ 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(
+ ValueError
+ ):
+
+ 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
+
+ def test_interface_collision(self) -> None:
+ with self.subTest("No collision"):
+ class NonInterface(GoodDbusInterface):
+ def do_work(self) -> None:
+ ...
+
+ with self.subTest("Collision"), self.assertRaises(ValueError):
+ class NewExampleInterface(
+ DbusInterfaceCommon,
+ interface_name="org.example.test",
+ ):
+ ...
+
+ def test_bad_class_names(self) -> None:
+ skip_if_no_name_validations()
+
+ 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"
+
+ 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:
+ ...
+
+ 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()
diff --git a/test/test_sdbus_utils.py b/test/test_sdbus_utils.py
new file mode 100644
index 0000000..49b443c
--- /dev/null
+++ b/test/test_sdbus_utils.py
@@ -0,0 +1,281 @@
+# 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 unittest import TestCase
+
+from sdbus.unittest import IsolatedDbusTestCase
+from sdbus.utils.inspect import inspect_dbus_bus, 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))
+
+ 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:
+ 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 = FooBarAsync()
+
+ 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)
+
+ 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)
diff --git a/test/test_typing.py b/test/test_typing.py
new file mode 100644
index 0000000..e6e956c
--- /dev/null
+++ b/test/test_typing.py
@@ -0,0 +1,167 @@
+# 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 import (
+ DbusInterfaceCommon,
+ DbusInterfaceCommonAsync,
+ dbus_method,
+ dbus_method_async,
+ dbus_property,
+ dbus_property_async,
+ dbus_signal_async,
+)
+
+
+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 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
+ )
diff --git a/tools/run_py_linters.py b/tools/run_py_linters.py
index a9e858e..091ae00 100755
--- a/tools/run_py_linters.py
+++ b/tools/run_py_linters.py
@@ -22,8 +22,7 @@
from argparse import ArgumentParser
from os import environ
from pathlib import Path
-from subprocess import run
-from typing import List
+from subprocess import SubprocessError, run
source_root = Path(environ['MESON_SOURCE_ROOT'])
build_dir = Path(environ['MESON_BUILD_ROOT'])
@@ -49,9 +48,9 @@ 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',
+ '--python-version', '3.9',
'--namespace-packages',
'--explicit-package-bases',
*all_python_modules,
@@ -61,7 +60,7 @@ def run_mypy() -> None:
)
-def linter_main() -> None:
+def run_flake8() -> None:
run(
args=(
'flake8',
@@ -70,11 +69,26 @@ 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
-def get_all_python_files() -> List[Path]:
- python_files: List[Path] = [source_root / 'setup.py']
+ if not is_success:
+ raise SystemExit(1)
+
+
+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():
@@ -88,10 +102,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,
)
@@ -100,7 +113,7 @@ def formater_main() -> None:
'isort',
'-m', 'VERTICAL_HANGING_INDENT',
'--trailing-comma',
- *all_python_files,
+ *all_python_modules,
),
check=True,
)
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
-
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()))
diff --git a/wheel-build/build_container_archive.py b/wheel-build/build_container_archive.py
deleted file mode 100755
index 9a3ebcf..0000000
--- a/wheel-build/build_container_archive.py
+++ /dev/null
@@ -1,150 +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
-from tempfile import TemporaryDirectory
-
-SYSTEMD_VERSION = '249.12'
-UTIL_LINUX_VERSION = '2.37'
-NINJA_VERSION = '1.10.2'
-LIBCAP_VERSION = '2.64'
-
-
-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_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_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_src_dir = build_dir / "src_systemd"
- systemd_src_dir.mkdir(exist_ok=True)
-
- util_linux_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"
-
- 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"
-
- 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'
-
- 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)
-
-
-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/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/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 b69e860..0000000
--- a/wheel-build/run_inside_container.py
+++ /dev/null
@@ -1,246 +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
-from typing import List
-
-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', 'cp38-cp38', 'cp37-cp37m']
-
-BASIC_C_FLAGS: List[str] = [
- '-O2', '-fno-plt', '-D_FORTIFY_SOURCE=2',
- '-fstack-clash-protection',
-]
-
-
-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_src_path = ROOT_DIR / 'src_ninja'
- 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==0.62', '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,
- 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:
- libcap_src_path = ROOT_DIR / 'src_libcap'
-
- run(
- ['make', '--jobs', NPROC, 'install'],
- 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,
- '-Dstatic-libsystemd=pic',
- '--buildtype', 'plain',
- '-Db_lto=true', '-Db_pie=true',
- ],
- 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:
- 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()
diff --git a/wheel-build/run_podman_full_build.py b/wheel-build/run_podman_full_build.py
new file mode 100644
index 0000000..991706e
--- /dev/null
+++ b/wheel-build/run_podman_full_build.py
@@ -0,0 +1,349 @@
+# 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 argparse import ArgumentParser
+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 = "docker.io/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.22"
+SYSTEMD_SRC_DIR = Path("/root/systemd")
+SYSTEMD_BUILD_DIR = SYSTEMD_SRC_DIR / "build"
+SYSTEMD_COMPAT_PATCHES: list[str] = [
+ "systemd_no_gettid_no_getdents64.patch",
+ "consistent_interface_order.patch",
+]
+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, 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", src_str, dest_str)
+ )
+
+
+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",
+ "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)
+ 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:
+ 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:
+ 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",
+ "--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,
+ "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,
+ "copy_dist": copy_dist,
+}
+
+
+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:
+ 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():
+ 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)