├── .flake8 ├── .gitignore ├── .style.yapf ├── .travis.yml ├── CHANGELOG.md ├── Dockerfile ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.md ├── bench └── unmarshall.py ├── dbus_next ├── __init__.py ├── __version__.py ├── _private │ ├── __init__.py │ ├── address.py │ ├── constants.py │ ├── marshaller.py │ ├── unmarshaller.py │ └── util.py ├── aio │ ├── __init__.py │ ├── message_bus.py │ └── proxy_object.py ├── auth.py ├── constants.py ├── errors.py ├── glib │ ├── __init__.py │ ├── message_bus.py │ └── proxy_object.py ├── introspection.py ├── message.py ├── message_bus.py ├── proxy_object.py ├── py.typed ├── service.py ├── signature.py └── validators.py ├── docs ├── Makefile ├── _static │ └── .gitignore ├── _templates │ └── .gitignore ├── authentication.rst ├── conf.py ├── constants.rst ├── errors.rst ├── high-level-client │ ├── aio-proxy-interface.rst │ ├── aio-proxy-object.rst │ ├── base-proxy-interface.rst │ ├── base-proxy-object.rst │ ├── glib-proxy-interface.rst │ ├── glib-proxy-object.rst │ └── index.rst ├── high-level-service │ ├── index.rst │ └── service-interface.rst ├── index.rst ├── introspection.rst ├── low-level-interface │ ├── index.rst │ └── message.rst ├── message-bus │ ├── aio-message-bus.rst │ ├── base-message-bus.rst │ ├── glib-message-bus.rst │ └── index.rst ├── type-system │ ├── index.rst │ ├── signature-tree.rst │ ├── signature-type.rst │ └── variant.rst └── validators.rst ├── examples ├── aio-list-names.py ├── aio-tcp-notification.py ├── dbus-next-send.py ├── example-service.py ├── glib-list-names.py └── mpris.py ├── pytest.ini ├── requirements-dev.txt ├── requirements.txt ├── setup.py └── test ├── __init__.py ├── client ├── test_methods.py ├── test_properties.py └── test_signals.py ├── data ├── introspection.xml └── messages.json ├── service ├── __init__.py ├── test_decorators.py ├── test_export.py ├── test_methods.py ├── test_properties.py ├── test_signals.py └── test_standard_interfaces.py ├── test_address_parser.py ├── test_aio_low_level.py ├── test_big_message.py ├── test_disconnect.py ├── test_fd_passing.py ├── test_glib_low_level.py ├── test_introspection.py ├── test_marshaller.py ├── test_request_name.py ├── test_signature.py ├── test_tcp_address.py ├── test_validators.py └── util.py /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore= 3 | E501 4 | E126 5 | E402 6 | F722 7 | 8 | # F821 is still relevant, but causes too many false positives in tests and 9 | # examples 10 | per-file-ignores= 11 | test/*:F821 12 | test/util.py:F401 13 | examples/*:F821 14 | */__init__.py:F401 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | 53 | # Translations 54 | *.mo 55 | *.pot 56 | 57 | # Django stuff: 58 | *.log 59 | local_settings.py 60 | db.sqlite3 61 | 62 | # Flask stuff: 63 | instance/ 64 | .webassets-cache 65 | 66 | # Scrapy stuff: 67 | .scrapy 68 | 69 | # Sphinx documentation 70 | docs/_build/ 71 | 72 | # PyBuilder 73 | target/ 74 | 75 | # Jupyter Notebook 76 | .ipynb_checkpoints 77 | 78 | # IPython 79 | profile_default/ 80 | ipython_config.py 81 | 82 | # pyenv 83 | .python-version 84 | 85 | # pipenv 86 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 87 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 88 | # having no cross-platform support, pipenv may install dependencies that don’t work, or not 89 | # install all needed dependencies. 90 | #Pipfile.lock 91 | 92 | # celery beat schedule file 93 | celerybeat-schedule 94 | 95 | # SageMath parsed files 96 | *.sage.py 97 | 98 | # Environments 99 | .env 100 | .venv 101 | env/ 102 | venv/ 103 | ENV/ 104 | env.bak/ 105 | venv.bak/ 106 | 107 | # Spyder project settings 108 | .spyderproject 109 | .spyproject 110 | 111 | # Rope project settings 112 | .ropeproject 113 | 114 | # mkdocs documentation 115 | /site 116 | 117 | # mypy 118 | .mypy_cache/ 119 | .dmypy.json 120 | dmypy.json 121 | 122 | # Pyre type checker 123 | .pyre/ 124 | 125 | # vim 126 | *.swp 127 | -------------------------------------------------------------------------------- /.style.yapf: -------------------------------------------------------------------------------- 1 | [style] 2 | column_limit = 100 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: minimal 2 | sudo: required 3 | dist: xenial 4 | services: 5 | - docker 6 | before_install: 7 | - docker build -t dbus-next-test . 8 | script: 9 | - docker run -it dbus-next-test 10 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## Version 0.2.3 4 | 5 | This version contains some bugfixes and new features. 6 | 7 | * Include py.typed in Manifest.in (#79) 8 | * Fix property validation error message (#81) 9 | * Don't log errors if call failed after disconnect (#84) 10 | * Support PEP563 annotations (#3) 11 | * Service: support async properties (#86) 12 | * Client: Support coroutines as signal handlers (#2) 13 | 14 | ## Version 0.2.2 15 | 16 | This version contains some bugfixes and a new feature 17 | 18 | * Add `connected` instance variable to the `MessageBus` (#74) 19 | * Better handling of message bus errors on disconnect (de8ed30) 20 | * Ensure futures are not done when settings results and exceptions (#73, 1213667) 21 | 22 | ## Version 0.2.1 23 | 24 | This version adds performance optimizations, bugfixes, and new features. 25 | 26 | * aio.MessageBus: Support passing unix fds. (#54) 27 | * Unmarshaller optimizations for a significant performance increase in message reading. (#62, #64) 28 | * Cache instances of `SignatureTree`. (ace5584) 29 | * Fix socket creation on macos. (#63) 30 | * Implement PEP 561 to indicate inline type hints. (#69) 31 | * aio.MessageBus: Return a future from `send()`. (302511b) 32 | * aio.MessageBus: Add `wait_for_disconnect()` to detect connection errors. (ab01ab1) 33 | 34 | Notice: `aio.MessageBus.send()` will be changed to a coroutine function in the 1.0 version of this library. 35 | 36 | ## Version 0.1.4 37 | 38 | This version adds some bugfixes and new features. 39 | 40 | * Support tcp transport addresses (#57) 41 | * Add support for the annonymous authentication protocol (#32) 42 | * Add flags kwarg to aio high level client method call (#55) 43 | * Allow subclassing of DBusError (#42) 44 | * Fix exception in aio message handler loop on task cancellation (ff165aa) 45 | * Improve error messages (#46, #59) 46 | * Fix match rule memory leak bug (508edf8) 47 | * Don't add match rules for high level client by default (615218f) 48 | * Add empty properties interface to standard interfaces (#49) 49 | 50 | ## Version 0.1.3 51 | 52 | This version adds some bugfixes and new features. 53 | 54 | * Add the object manager interface to the service. (#14, #37) 55 | * Allow coroutines in service methods. (#24, #27) 56 | * Client: don't send method replies with `NO_REPLY_EXPECTED` message flag. (#22) 57 | * Fix duplicate nodes in introspection. (#13) 58 | 59 | ## Version 0.1.2 60 | 61 | This version adds some bugfixes. 62 | 63 | * Allow exporting interface multiple times (#4) 64 | * Fix super call in exceptions (#5) 65 | * Add timeout support on `introspect` (#7) 66 | * Add unix fd type 'h' to valid tokens (#9) 67 | * Dont use future annotations (#10) 68 | * Fix variant validator (d724fc2) 69 | 70 | ## Version 0.1.1 71 | 72 | This version adds some major features and breaking changes. 73 | 74 | * Remove the MessageBus convenience constructors (breaking). 75 | * Complete documentation. 76 | * Type annotation for all public methods. 77 | 78 | ## Version 0.0.1 79 | 80 | This is the first release of python-dbus-next. 81 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:20.04 2 | 3 | WORKDIR /app 4 | 5 | RUN echo force-unsafe-io > /etc/dpkg/dpkg.cfg.d/docker-apt-speedup 6 | RUN echo 'APT::Acquire::Retries "5";' > /etc/apt/apt.conf.d/80retry 7 | 8 | RUN export DEBIAN_FRONTEND=noninteractive; \ 9 | export DEBCONF_NONINTERACTIVE_SEEN=true; \ 10 | echo 'tzdata tzdata/Areas select Etc' | debconf-set-selections; \ 11 | echo 'tzdata tzdata/Zones/Etc select UTC' | debconf-set-selections; \ 12 | apt update && \ 13 | apt install software-properties-common -y --no-install-recommends && \ 14 | add-apt-repository ppa:deadsnakes/ppa && \ 15 | apt update && apt install -y --no-install-recommends \ 16 | build-essential \ 17 | python3-pip \ 18 | python3 \ 19 | python3.7 \ 20 | python3.7-distutils \ 21 | python3.9 \ 22 | python3.9-distutils \ 23 | python3.10 \ 24 | python3.10-distutils \ 25 | curl \ 26 | dbus \ 27 | python3-gi 28 | 29 | RUN set -e -x; \ 30 | pip3 install 'yapf==0.31' 'flake8==4.0.1'; \ 31 | curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py; \ 32 | for py in python3.7 python3.8 python3.9 python3.10; do \ 33 | ${py} get-pip.py; \ 34 | PYTHONPATH=/usr/lib/${py}/site-packages ${py} -m pip install \ 35 | 'pytest==6.2.5' \ 36 | 'pytest-asyncio==0.16.0' \ 37 | 'pytest-timeout==2.0.2' \ 38 | 'pytest-cov==3.0.0'; \ 39 | done 40 | 41 | ADD . /app 42 | 43 | CMD ["make", "clean", "test", "check"] 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019 Tony Crisci 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md LICENSE CHANGELOG.md pytest.ini requirements.txt .flake8 .style.yapf Dockerfile dbus_next/py.typed 2 | recursive-include test *.py 3 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: lint check format test docker-test clean publish docs livedocs all 2 | .DEFAULT_GOAL := all 3 | 4 | source_dirs = dbus_next test examples 5 | 6 | lint: 7 | python3 -m flake8 $(source_dirs) 8 | 9 | check: lint 10 | python3 -m yapf -rdp $(source_dirs) 11 | 12 | format: 13 | python3 -m yapf -rip $(source_dirs) 14 | 15 | test: 16 | for py in python3.7 python3.9 python3.10 python3.8 ; do \ 17 | if hash $${py}; then \ 18 | PYTHONPATH=/usr/lib/$${py}/site-packages dbus-run-session $${py} -m pytest -sv --cov=dbus_next || exit 1 ; \ 19 | fi \ 20 | done \ 21 | 22 | docker-test: 23 | docker build -t dbus-next-test . 24 | docker run -it dbus-next-test 25 | 26 | clean: 27 | rm -rf dist dbus_next.egg-info build docs/_build 28 | rm -rf `find -type d -name __pycache__` 29 | 30 | publish: 31 | python3 setup.py sdist bdist_wheel 32 | python3 -m twine upload --repository-url https://upload.pypi.org/legacy/ dist/* 33 | 34 | docs: 35 | sphinx-build docs docs/_build/html 36 | 37 | livedocs: 38 | sphinx-autobuild docs docs/_build/html --watch dbus_next 39 | 40 | all: format lint test 41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # python-dbus-next 2 | 3 | The next great DBus library for Python. 4 | 5 | [Documentation](https://python-dbus-next.readthedocs.io/en/latest/) 6 | 7 | [Chat](https://discord.gg/UdbXHVX) 8 | 9 | python-dbus-next is a Python library for DBus that aims to be a fully featured high level library primarily geared towards integration of applications into Linux desktop and mobile environments. 10 | 11 | Desktop application developers can use this library for integrating their applications into desktop environments by implementing common DBus standard interfaces or creating custom plugin interfaces. 12 | 13 | Desktop users can use this library to create their own scripts and utilities to interact with those interfaces for customization of their desktop environment. 14 | 15 | python-dbus-next plans to improve over other DBus libraries for Python in the following ways: 16 | 17 | * Zero dependencies and pure Python 3. 18 | * Support for multiple IO backends including asyncio and the GLib main loop. 19 | * Nonblocking IO suitable for GUI development. 20 | * Target the latest language features of Python for beautiful services and clients. 21 | * Complete implementation of the DBus type system without ever guessing types. 22 | * Integration tests for all features of the library. 23 | * Completely documented public API. 24 | 25 | ## Installing 26 | 27 | This library is available on PyPi as [dbus-next](https://pypi.org/project/dbus-next/). 28 | 29 | ``` 30 | pip3 install dbus-next 31 | ``` 32 | 33 | ## The Client Interface 34 | 35 | To use a service on the bus, the library constructs a proxy object you can use to call methods, get and set properties, and listen to signals. 36 | 37 | For more information, see the [overview for the high-level client](https://python-dbus-next.readthedocs.io/en/latest/high-level-client/index.html). 38 | 39 | This example connects to a media player and controls it with the [MPRIS](https://specifications.freedesktop.org/mpris-spec/latest/) DBus interface. 40 | 41 | ```python 42 | from dbus_next.aio import MessageBus 43 | 44 | import asyncio 45 | 46 | loop = asyncio.get_event_loop() 47 | 48 | 49 | async def main(): 50 | bus = await MessageBus().connect() 51 | # the introspection xml would normally be included in your project, but 52 | # this is convenient for development 53 | introspection = await bus.introspect('org.mpris.MediaPlayer2.vlc', '/org/mpris/MediaPlayer2') 54 | 55 | obj = bus.get_proxy_object('org.mpris.MediaPlayer2.vlc', '/org/mpris/MediaPlayer2', introspection) 56 | player = obj.get_interface('org.mpris.MediaPlayer2.Player') 57 | properties = obj.get_interface('org.freedesktop.DBus.Properties') 58 | 59 | # call methods on the interface (this causes the media player to play) 60 | await player.call_play() 61 | 62 | volume = await player.get_volume() 63 | print(f'current volume: {volume}, setting to 0.5') 64 | 65 | await player.set_volume(0.5) 66 | 67 | # listen to signals 68 | def on_properties_changed(interface_name, changed_properties, invalidated_properties): 69 | for changed, variant in changed_properties.items(): 70 | print(f'property changed: {changed} - {variant.value}') 71 | 72 | properties.on_properties_changed(on_properties_changed) 73 | 74 | await loop.create_future() 75 | 76 | loop.run_until_complete(main()) 77 | ``` 78 | 79 | ## The Service Interface 80 | 81 | To define a service on the bus, use the `ServiceInterface` class and decorate class methods to specify DBus methods, properties, and signals with their type signatures. 82 | 83 | For more information, see the [overview for the high-level service](https://python-dbus-next.readthedocs.io/en/latest/high-level-service/index.html). 84 | 85 | ```python 86 | from dbus_next.service import ServiceInterface, method, dbus_property, signal, Variant 87 | from dbus_next.aio import MessageBus 88 | 89 | import asyncio 90 | 91 | class ExampleInterface(ServiceInterface): 92 | def __init__(self, name): 93 | super().__init__(name) 94 | self._string_prop = 'kevin' 95 | 96 | @method() 97 | def Echo(self, what: 's') -> 's': 98 | return what 99 | 100 | @method() 101 | def GetVariantDict() -> 'a{sv}': 102 | return { 103 | 'foo': Variant('s', 'bar'), 104 | 'bat': Variant('x', -55), 105 | 'a_list': Variant('as', ['hello', 'world']) 106 | } 107 | 108 | @dbus_property() 109 | def string_prop(self) -> 's': 110 | return self._string_prop 111 | 112 | @string_prop.setter 113 | def string_prop_setter(self, val: 's'): 114 | self._string_prop = val 115 | 116 | @signal() 117 | def signal_simple(self) -> 's': 118 | return 'hello' 119 | 120 | async def main(): 121 | bus = await MessageBus().connect() 122 | interface = ExampleInterface('test.interface') 123 | bus.export('/test/path', interface) 124 | # now that we are ready to handle requests, we can request name from D-Bus 125 | await bus.request_name('test.name') 126 | # wait indefinitely 127 | await asyncio.get_event_loop().create_future() 128 | 129 | asyncio.get_event_loop().run_until_complete(main()) 130 | ``` 131 | 132 | ## The Low-Level Interface 133 | 134 | The low-level interface works with DBus messages directly. 135 | 136 | For more information, see the [overview for the low-level interface](https://python-dbus-next.readthedocs.io/en/latest/low-level-interface/index.html). 137 | 138 | ```python 139 | from dbus_next.message import Message, MessageType 140 | from dbus_next.aio import MessageBus 141 | 142 | import asyncio 143 | import json 144 | 145 | loop = asyncio.get_event_loop() 146 | 147 | 148 | async def main(): 149 | bus = await MessageBus().connect() 150 | 151 | reply = await bus.call( 152 | Message(destination='org.freedesktop.DBus', 153 | path='/org/freedesktop/DBus', 154 | interface='org.freedesktop.DBus', 155 | member='ListNames')) 156 | 157 | if reply.message_type == MessageType.ERROR: 158 | raise Exception(reply.body[0]) 159 | 160 | print(json.dumps(reply.body[0], indent=2)) 161 | 162 | 163 | loop.run_until_complete(main()) 164 | ``` 165 | 166 | ## Projects that use python-dbus-next 167 | 168 | * The [Playerctl](https://github.com/altdesktop/playerctl) test suite 169 | * [i3-dstatus](https://github.com/altdesktop/i3-dstatus) 170 | 171 | ## Contributing 172 | 173 | Contributions are welcome. Development happens on [Github](https://github.com/altdesktop/python-dbus-next). 174 | 175 | Before you commit, run `make` to run the linter, code formatter, and the test suite. 176 | 177 | # Copyright 178 | 179 | You can use this code under an MIT license (see LICENSE). 180 | 181 | © 2019, Tony Crisci 182 | -------------------------------------------------------------------------------- /bench/unmarshall.py: -------------------------------------------------------------------------------- 1 | import io 2 | import timeit 3 | 4 | from dbus_next._private.unmarshaller import Unmarshaller 5 | 6 | bluez_rssi_message = ( 7 | "6c04010134000000e25389019500000001016f00250000002f6f72672f626c75657a2f686369302f6465" 8 | "765f30385f33415f46325f31455f32425f3631000000020173001f0000006f72672e667265656465736b" 9 | "746f702e444275732e50726f7065727469657300030173001100000050726f706572746965734368616e" 10 | "67656400000000000000080167000873617b73767d617300000007017300040000003a312e3400000000" 11 | "110000006f72672e626c75657a2e446576696365310000000e0000000000000004000000525353490001" 12 | "6e00a7ff000000000000" 13 | ) 14 | 15 | 16 | def unmarhsall_bluez_rssi_message(): 17 | Unmarshaller(io.BytesIO(bytes.fromhex(bluez_rssi_message))).unmarshall() 18 | 19 | 20 | count = 1000000 21 | time = timeit.Timer(unmarhsall_bluez_rssi_message).timeit(count) 22 | print(f"Unmarshalling {count} bluetooth rssi messages took {time} seconds") 23 | -------------------------------------------------------------------------------- /dbus_next/__init__.py: -------------------------------------------------------------------------------- 1 | from . import aio 2 | from . import glib 3 | from .constants import (BusType, MessageType, MessageFlag, NameFlag, RequestNameReply, 4 | ReleaseNameReply, PropertyAccess, ArgDirection, ErrorType) 5 | from .errors import (SignatureBodyMismatchError, InvalidSignatureError, InvalidAddressError, 6 | AuthError, InvalidMessageError, InvalidIntrospectionError, 7 | InterfaceNotFoundError, SignalDisabledError, InvalidBusNameError, 8 | InvalidObjectPathError, InvalidInterfaceNameError, InvalidMemberNameError, 9 | DBusError) 10 | from . import introspection 11 | from .message import Message 12 | from . import message_bus 13 | from . import proxy_object 14 | from . import service 15 | from .signature import SignatureType, SignatureTree, Variant 16 | from .validators import (is_bus_name_valid, is_object_path_valid, is_interface_name_valid, 17 | is_member_name_valid, assert_bus_name_valid, assert_object_path_valid, 18 | assert_interface_name_valid, assert_member_name_valid) 19 | -------------------------------------------------------------------------------- /dbus_next/__version__.py: -------------------------------------------------------------------------------- 1 | __title__ = 'dbus_next' 2 | __description__ = 'A zero-dependency DBus library for Python with asyncio support' 3 | __url__ = 'https://github.com/altdesktop/python-dbus-next' 4 | __version__ = '0.2.3' 5 | __author__ = 'Tony Crisci' 6 | __author_email__ = 'tony@dubstepdish.com' 7 | __license__ = 'MIT' 8 | __copyright__ = 'Copyright 2019 Tony Crisci' 9 | -------------------------------------------------------------------------------- /dbus_next/_private/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/altdesktop/python-dbus-next/ab566e16a71bfc9d7e0d29676aa459ec060e72c5/dbus_next/_private/__init__.py -------------------------------------------------------------------------------- /dbus_next/_private/address.py: -------------------------------------------------------------------------------- 1 | from ..constants import BusType 2 | from ..errors import InvalidAddressError 3 | 4 | from urllib.parse import unquote 5 | import re 6 | import os 7 | 8 | invalid_address_chars_re = re.compile(r'[^-0-9A-Za-z_/.%]') 9 | 10 | 11 | def parse_address(address_str): 12 | addresses = [] 13 | 14 | for address in filter(lambda a: a, address_str.split(';')): 15 | if address.find(':') == -1: 16 | raise InvalidAddressError('address did not contain a transport') 17 | 18 | transport, opt_string = address.split(':', 1) 19 | options = {} 20 | 21 | for kv in filter(lambda s: s, opt_string.split(',')): 22 | if kv.find('=') == -1: 23 | raise InvalidAddressError('address option did not contain a value') 24 | k, v = kv.split('=', 1) 25 | if invalid_address_chars_re.search(v): 26 | raise InvalidAddressError('address contains invalid characters') 27 | # XXX the actual unquote rules are simpler than this 28 | v = unquote(v) 29 | options[k] = v 30 | 31 | addresses.append((transport, options)) 32 | 33 | if not addresses: 34 | raise InvalidAddressError(f'address string contained no addresses: "{address_str}"') 35 | 36 | return addresses 37 | 38 | 39 | def get_system_bus_address(): 40 | if 'DBUS_SYSTEM_BUS_ADDRESS' in os.environ: 41 | return os.environ['DBUS_SYSTEM_BUS_ADDRESS'] 42 | else: 43 | return 'unix:path=/var/run/dbus/system_bus_socket' 44 | 45 | 46 | display_re = re.compile(r'.*:([0-9]+)\.?.*') 47 | remove_quotes_re = re.compile(r'''^['"]?(.*?)['"]?$''') 48 | 49 | 50 | def get_session_bus_address(): 51 | if 'DBUS_SESSION_BUS_ADDRESS' in os.environ: 52 | return os.environ['DBUS_SESSION_BUS_ADDRESS'] 53 | 54 | home = os.environ['HOME'] 55 | if 'DISPLAY' not in os.environ: 56 | raise InvalidAddressError( 57 | 'DBUS_SESSION_BUS_ADDRESS not set and could not get DISPLAY environment variable to get bus address' 58 | ) 59 | 60 | display = os.environ['DISPLAY'] 61 | try: 62 | display = display_re.search(display).group(1) 63 | except Exception: 64 | raise InvalidAddressError( 65 | f'DBUS_SESSION_BUS_ADDRESS not set and could not parse DISPLAY environment variable to get bus address: {display}' 66 | ) 67 | 68 | # XXX: this will block but they're very small files and fs operations 69 | # should be fairly reliable. fix this by passing in an async func to read 70 | # the file for each io backend. 71 | machine_id = None 72 | with open('/var/lib/dbus/machine-id') as f: 73 | machine_id = f.read().rstrip() 74 | 75 | dbus_info_file_name = f'{home}/.dbus/session-bus/{machine_id}-{display}' 76 | dbus_info = None 77 | try: 78 | with open(dbus_info_file_name) as f: 79 | dbus_info = f.read().rstrip() 80 | except Exception: 81 | raise InvalidAddressError(f'could not open dbus info file: {dbus_info_file_name}') 82 | 83 | for line in dbus_info.split('\n'): 84 | if line.strip().startswith('DBUS_SESSION_BUS_ADDRESS='): 85 | _, addr = line.split('=', 1) 86 | if not addr: 87 | raise InvalidAddressError( 88 | f'DBUS_SESSION_BUS_ADDRESS variable not set correctly in dbus info file: {dbus_info_file_name}' 89 | ) 90 | addr = remove_quotes_re.search(addr).group(1) 91 | return addr 92 | 93 | raise InvalidAddressError('could not find dbus session bus address') 94 | 95 | 96 | def get_bus_address(bus_type): 97 | if bus_type == BusType.SESSION: 98 | return get_session_bus_address() 99 | elif bus_type == BusType.SYSTEM: 100 | return get_system_bus_address() 101 | else: 102 | raise Exception('got unknown bus type: {bus_type}') 103 | -------------------------------------------------------------------------------- /dbus_next/_private/constants.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | PROTOCOL_VERSION = 1 4 | 5 | LITTLE_ENDIAN = ord('l') 6 | BIG_ENDIAN = ord('B') 7 | 8 | 9 | class HeaderField(Enum): 10 | PATH = 1 11 | INTERFACE = 2 12 | MEMBER = 3 13 | ERROR_NAME = 4 14 | REPLY_SERIAL = 5 15 | DESTINATION = 6 16 | SENDER = 7 17 | SIGNATURE = 8 18 | UNIX_FDS = 9 19 | 20 | 21 | HEADER_NAME_MAP = {field.value: field.name for field in HeaderField} 22 | -------------------------------------------------------------------------------- /dbus_next/_private/marshaller.py: -------------------------------------------------------------------------------- 1 | from ..signature import SignatureTree 2 | from struct import pack 3 | 4 | 5 | class Marshaller: 6 | def __init__(self, signature, body): 7 | self.signature_tree = SignatureTree._get(signature) 8 | self.signature_tree.verify(body) 9 | self.buffer = bytearray() 10 | self.body = body 11 | 12 | self.writers = { 13 | 'y': self.write_byte, 14 | 'b': self.write_boolean, 15 | 'n': self.write_int16, 16 | 'q': self.write_uint16, 17 | 'i': self.write_int32, 18 | 'u': self.write_uint32, 19 | 'x': self.write_int64, 20 | 't': self.write_uint64, 21 | 'd': self.write_double, 22 | 'h': self.write_uint32, 23 | 'o': self.write_string, 24 | 's': self.write_string, 25 | 'g': self.write_signature, 26 | 'a': self.write_array, 27 | '(': self.write_struct, 28 | '{': self.write_dict_entry, 29 | 'v': self.write_variant 30 | } 31 | 32 | def align(self, n): 33 | offset = n - len(self.buffer) % n 34 | if offset == 0 or offset == n: 35 | return 0 36 | self.buffer.extend(bytes(offset)) 37 | return offset 38 | 39 | def write_byte(self, byte, _=None): 40 | self.buffer.append(byte) 41 | return 1 42 | 43 | def write_boolean(self, boolean, _=None): 44 | if boolean: 45 | return self.write_uint32(1) 46 | else: 47 | return self.write_uint32(0) 48 | 49 | def write_int16(self, int16, _=None): 50 | written = self.align(2) 51 | self.buffer.extend(pack(' bool: 9 | '''For a given signature and body, check to see if it contains any members 10 | with the given token''' 11 | if type(signature) is str: 12 | signature = SignatureTree._get(signature) 13 | 14 | queue = [] 15 | contains_variants = False 16 | for st in signature.types: 17 | queue.append(st) 18 | 19 | while True: 20 | if not queue: 21 | break 22 | st = queue.pop() 23 | if st.token == token: 24 | return True 25 | elif st.token == 'v': 26 | contains_variants = True 27 | queue.extend(st.children) 28 | 29 | if not contains_variants: 30 | return False 31 | 32 | for member in body: 33 | queue.append(member) 34 | 35 | while True: 36 | if not queue: 37 | return False 38 | member = queue.pop() 39 | if type(member) is Variant and \ 40 | signature_contains_type(member.signature, [member.value], token): 41 | return True 42 | elif type(member) is list: 43 | queue.extend(member) 44 | elif type(member) is dict: 45 | queue.extend(member.values()) 46 | 47 | 48 | def replace_fds_with_idx(signature: Union[str, SignatureTree], 49 | body: List[Any]) -> (List[Any], List[int]): 50 | '''Take the high level body format and convert it into the low level body 51 | format. Type 'h' refers directly to the fd in the body. Replace that with 52 | an index and return the corresponding list of unix fds that can be set on 53 | the Message''' 54 | if type(signature) is str: 55 | signature = SignatureTree._get(signature) 56 | 57 | if not signature_contains_type(signature, body, 'h'): 58 | return body, [] 59 | 60 | unix_fds = [] 61 | 62 | def _replace(fd): 63 | try: 64 | return unix_fds.index(fd) 65 | except ValueError: 66 | unix_fds.append(fd) 67 | return len(unix_fds) - 1 68 | 69 | _replace_fds(body, signature.types, _replace) 70 | 71 | return body, unix_fds 72 | 73 | 74 | def replace_idx_with_fds(signature: Union[str, SignatureTree], body: List[Any], 75 | unix_fds: List[int]) -> List[Any]: 76 | '''Take the low level body format and return the high level body format. 77 | Type 'h' refers to an index in the unix_fds array. Replace those with the 78 | actual file descriptor or `None` if one does not exist.''' 79 | if type(signature) is str: 80 | signature = SignatureTree._get(signature) 81 | 82 | if not signature_contains_type(signature, body, 'h'): 83 | return body 84 | 85 | def _replace(idx): 86 | try: 87 | return unix_fds[idx] 88 | except IndexError: 89 | return None 90 | 91 | _replace_fds(body, signature.types, _replace) 92 | 93 | return body 94 | 95 | 96 | def parse_annotation(annotation: str) -> str: 97 | ''' 98 | Because of PEP 563, if `from __future__ import annotations` is used in code 99 | or on Python version >=3.10 where this is the default, return annotations 100 | from the `inspect` module will return annotations as "forward definitions". 101 | In this case, we must eval the result which we do only when given a string 102 | constant. 103 | ''' 104 | def raise_value_error(): 105 | raise ValueError(f'service annotations must be a string constant (got {annotation})') 106 | 107 | if not annotation or annotation is inspect.Signature.empty: 108 | return '' 109 | if type(annotation) is not str: 110 | raise_value_error() 111 | try: 112 | body = ast.parse(annotation).body 113 | if len(body) == 1 and type(body[0].value) is ast.Constant: 114 | if type(body[0].value.value) is not str: 115 | raise_value_error() 116 | return body[0].value.value 117 | except SyntaxError: 118 | pass 119 | 120 | return annotation 121 | 122 | 123 | def _replace_fds(body_obj: List[Any], children, replace_fn): 124 | '''Replace any type 'h' with the value returned by replace_fn() given the 125 | value of the fd field. This is used by the high level interfaces which 126 | allow type 'h' to be the fd directly instead of an index in an external 127 | array such as in the spec.''' 128 | for index, st in enumerate(children): 129 | if not any(sig in st.signature for sig in 'hv'): 130 | continue 131 | if st.signature == 'h': 132 | body_obj[index] = replace_fn(body_obj[index]) 133 | elif st.token == 'a': 134 | if st.children[0].token == '{': 135 | _replace_fds(body_obj[index], st.children, replace_fn) 136 | else: 137 | for i, child in enumerate(body_obj[index]): 138 | if st.signature == 'ah': 139 | body_obj[index][i] = replace_fn(child) 140 | else: 141 | _replace_fds([child], st.children, replace_fn) 142 | elif st.token in '(': 143 | _replace_fds(body_obj[index], st.children, replace_fn) 144 | elif st.token in '{': 145 | for key, value in list(body_obj.items()): 146 | body_obj.pop(key) 147 | if st.children[0].signature == 'h': 148 | key = replace_fn(key) 149 | if st.children[1].signature == 'h': 150 | value = replace_fn(value) 151 | else: 152 | _replace_fds([value], [st.children[1]], replace_fn) 153 | body_obj[key] = value 154 | 155 | elif st.signature == 'v': 156 | if body_obj[index].signature == 'h': 157 | body_obj[index].value = replace_fn(body_obj[index].value) 158 | else: 159 | _replace_fds([body_obj[index].value], [body_obj[index].type], replace_fn) 160 | 161 | elif st.children: 162 | _replace_fds(body_obj[index], st.children, replace_fn) 163 | -------------------------------------------------------------------------------- /dbus_next/aio/__init__.py: -------------------------------------------------------------------------------- 1 | from .message_bus import MessageBus 2 | from .proxy_object import ProxyObject, ProxyInterface 3 | -------------------------------------------------------------------------------- /dbus_next/aio/proxy_object.py: -------------------------------------------------------------------------------- 1 | from ..proxy_object import BaseProxyObject, BaseProxyInterface 2 | from ..message_bus import BaseMessageBus 3 | from ..message import Message, MessageFlag 4 | from ..signature import Variant 5 | from ..errors import DBusError 6 | from ..constants import ErrorType 7 | from .._private.util import replace_idx_with_fds, replace_fds_with_idx 8 | from .. import introspection as intr 9 | import xml.etree.ElementTree as ET 10 | 11 | from typing import Union, List 12 | 13 | 14 | class ProxyInterface(BaseProxyInterface): 15 | """A class representing a proxy to an interface exported on the bus by 16 | another client for the asyncio :class:`MessageBus 17 | ` implementation. 18 | 19 | This class is not meant to be constructed directly by the user. Use 20 | :func:`ProxyObject.get_interface() 21 | ` on a asyncio proxy object to get 22 | a proxy interface. 23 | 24 | This class exposes methods to call DBus methods, listen to signals, and get 25 | and set properties on the interface that are created dynamically based on 26 | the introspection data passed to the proxy object that made this proxy 27 | interface. 28 | 29 | A *method call* takes this form: 30 | 31 | .. code-block:: python3 32 | 33 | result = await interface.call_[METHOD](*args) 34 | 35 | Where ``METHOD`` is the name of the method converted to snake case. 36 | 37 | DBus methods are exposed as coroutines that take arguments that correpond 38 | to the *in args* of the interface method definition and return a ``result`` 39 | that corresponds to the *out arg*. If the method has more than one out arg, 40 | they are returned within a :class:`list`. 41 | 42 | To *listen to a signal* use this form: 43 | 44 | .. code-block:: python3 45 | 46 | interface.on_[SIGNAL](callback) 47 | 48 | To *stop listening to a signal* use this form: 49 | 50 | .. code-block:: python3 51 | 52 | interface.off_[SIGNAL](callback) 53 | 54 | Where ``SIGNAL`` is the name of the signal converted to snake case. 55 | 56 | DBus signals are exposed with an event-callback interface. The provided 57 | ``callback`` will be called when the signal is emitted with arguments that 58 | correspond to the *out args* of the interface signal definition. 59 | 60 | To *get or set a property* use this form: 61 | 62 | .. code-block:: python3 63 | 64 | value = await interface.get_[PROPERTY]() 65 | await interface.set_[PROPERTY](value) 66 | 67 | Where ``PROPERTY`` is the name of the property converted to snake case. 68 | 69 | DBus property getters and setters are exposed as coroutines. The ``value`` 70 | must correspond to the type of the property in the interface definition. 71 | 72 | If the service returns an error for a DBus call, a :class:`DBusError 73 | ` will be raised with information about the error. 74 | """ 75 | def _add_method(self, intr_method): 76 | async def method_fn(*args, flags=MessageFlag.NONE): 77 | input_body, unix_fds = replace_fds_with_idx(intr_method.in_signature, list(args)) 78 | 79 | msg = await self.bus.call( 80 | Message(destination=self.bus_name, 81 | path=self.path, 82 | interface=self.introspection.name, 83 | member=intr_method.name, 84 | signature=intr_method.in_signature, 85 | body=input_body, 86 | flags=flags, 87 | unix_fds=unix_fds)) 88 | 89 | if flags & MessageFlag.NO_REPLY_EXPECTED: 90 | return None 91 | 92 | BaseProxyInterface._check_method_return(msg, intr_method.out_signature) 93 | 94 | out_len = len(intr_method.out_args) 95 | 96 | body = replace_idx_with_fds(msg.signature_tree, msg.body, msg.unix_fds) 97 | 98 | if not out_len: 99 | return None 100 | elif out_len == 1: 101 | return body[0] 102 | else: 103 | return body 104 | 105 | method_name = f'call_{BaseProxyInterface._to_snake_case(intr_method.name)}' 106 | setattr(self, method_name, method_fn) 107 | 108 | def _add_property(self, intr_property): 109 | async def property_getter(): 110 | msg = await self.bus.call( 111 | Message(destination=self.bus_name, 112 | path=self.path, 113 | interface='org.freedesktop.DBus.Properties', 114 | member='Get', 115 | signature='ss', 116 | body=[self.introspection.name, intr_property.name])) 117 | 118 | BaseProxyInterface._check_method_return(msg, 'v') 119 | variant = msg.body[0] 120 | if variant.signature != intr_property.signature: 121 | raise DBusError(ErrorType.CLIENT_ERROR, 122 | f'property returned unexpected signature "{variant.signature}"', 123 | msg) 124 | 125 | return replace_idx_with_fds('v', msg.body, msg.unix_fds)[0].value 126 | 127 | async def property_setter(val): 128 | variant = Variant(intr_property.signature, val) 129 | 130 | body, unix_fds = replace_fds_with_idx( 131 | 'ssv', [self.introspection.name, intr_property.name, variant]) 132 | 133 | msg = await self.bus.call( 134 | Message(destination=self.bus_name, 135 | path=self.path, 136 | interface='org.freedesktop.DBus.Properties', 137 | member='Set', 138 | signature='ssv', 139 | body=body, 140 | unix_fds=unix_fds)) 141 | 142 | BaseProxyInterface._check_method_return(msg) 143 | 144 | snake_case = BaseProxyInterface._to_snake_case(intr_property.name) 145 | setattr(self, f'get_{snake_case}', property_getter) 146 | setattr(self, f'set_{snake_case}', property_setter) 147 | 148 | 149 | class ProxyObject(BaseProxyObject): 150 | """The proxy object implementation for the GLib :class:`MessageBus `. 151 | 152 | For more information, see the :class:`BaseProxyObject `. 153 | """ 154 | def __init__(self, bus_name: str, path: str, introspection: Union[intr.Node, str, ET.Element], 155 | bus: BaseMessageBus): 156 | super().__init__(bus_name, path, introspection, bus, ProxyInterface) 157 | 158 | def get_interface(self, name: str) -> ProxyInterface: 159 | return super().get_interface(name) 160 | 161 | def get_children(self) -> List['ProxyObject']: 162 | return super().get_children() 163 | -------------------------------------------------------------------------------- /dbus_next/auth.py: -------------------------------------------------------------------------------- 1 | from .errors import AuthError 2 | import enum 3 | import os 4 | 5 | # The auth interface here is unstable. I would like to eventually open this up 6 | # for people to define their own custom authentication protocols, but I'm not 7 | # familiar with what's needed for that exactly. To work with any message bus 8 | # implementation would require abstracting out all the IO. Async operations 9 | # might be challenging because different IO backends have different ways of 10 | # doing that. I might just end up giving the raw socket and leaving it all up 11 | # to the user, but it would be nice to have a little guidance in the interface 12 | # since a lot of it is strongly specified. If you have a need for this, contact 13 | # the project maintainer to help stabalize this interface. 14 | 15 | 16 | class _AuthResponse(enum.Enum): 17 | OK = 'OK' 18 | REJECTED = 'REJECTED' 19 | DATA = 'DATA' 20 | ERROR = 'ERROR' 21 | AGREE_UNIX_FD = 'AGREE_UNIX_FD' 22 | 23 | @classmethod 24 | def parse(klass, line): 25 | args = line.split(' ') 26 | response = klass(args[0]) 27 | return response, args[1:] 28 | 29 | 30 | # UNSTABLE 31 | class Authenticator: 32 | """The base class for authenticators for :class:`MessageBus ` authentication. 33 | 34 | In the future, the library may allow extending this class for custom authentication protocols. 35 | 36 | :seealso: https://dbus.freedesktop.org/doc/dbus-specification.html#auth-protocol 37 | """ 38 | def _authentication_start(self, negotiate_unix_fd=False): 39 | raise NotImplementedError( 40 | 'authentication_start() must be implemented in the inheriting class') 41 | 42 | def _receive_line(self, line): 43 | raise NotImplementedError('receive_line() must be implemented in the inheriting class') 44 | 45 | @staticmethod 46 | def _format_line(line): 47 | return f'{line}\r\n'.encode() 48 | 49 | 50 | class AuthExternal(Authenticator): 51 | """An authenticator class for the external auth protocol for use with the 52 | :class:`MessageBus `. 53 | 54 | :sealso: https://dbus.freedesktop.org/doc/dbus-specification.html#auth-protocol 55 | """ 56 | def __init__(self): 57 | self.negotiate_unix_fd = False 58 | self.negotiating_fds = False 59 | 60 | def _authentication_start(self, negotiate_unix_fd=False) -> str: 61 | self.negotiate_unix_fd = negotiate_unix_fd 62 | hex_uid = str(os.getuid()).encode().hex() 63 | return f'AUTH EXTERNAL {hex_uid}' 64 | 65 | def _receive_line(self, line: str): 66 | response, args = _AuthResponse.parse(line) 67 | 68 | if response is _AuthResponse.OK: 69 | if self.negotiate_unix_fd: 70 | self.negotiating_fds = True 71 | return "NEGOTIATE_UNIX_FD" 72 | else: 73 | return "BEGIN" 74 | 75 | if response is _AuthResponse.AGREE_UNIX_FD: 76 | return "BEGIN" 77 | 78 | raise AuthError(f'authentication failed: {response.value}: {args}') 79 | 80 | 81 | class AuthAnnonymous(Authenticator): 82 | """An authenticator class for the annonymous auth protocol for use with the 83 | :class:`MessageBus `. 84 | 85 | :sealso: https://dbus.freedesktop.org/doc/dbus-specification.html#auth-protocol 86 | """ 87 | def _authentication_start(self, negotiate_unix_fd=False) -> str: 88 | if negotiate_unix_fd: 89 | raise AuthError( 90 | 'annonymous authentication does not support negotiating unix fds right now') 91 | 92 | return 'AUTH ANONYMOUS' 93 | 94 | def _receive_line(self, line: str) -> str: 95 | response, args = _AuthResponse.parse(line) 96 | 97 | if response != _AuthResponse.OK: 98 | raise AuthError(f'authentication failed: {response.value}: {args}') 99 | 100 | return 'BEGIN' 101 | -------------------------------------------------------------------------------- /dbus_next/constants.py: -------------------------------------------------------------------------------- 1 | from enum import Enum, IntFlag 2 | 3 | 4 | class BusType(Enum): 5 | """An enum that indicates a type of bus. On most systems, there are 6 | normally two different kinds of buses running. 7 | """ 8 | SESSION = 1 #: A bus for the current graphical user session. 9 | SYSTEM = 2 #: A persistent bus for the whole machine. 10 | 11 | 12 | class MessageType(Enum): 13 | """An enum that indicates a type of message.""" 14 | METHOD_CALL = 1 #: An outgoing method call. 15 | METHOD_RETURN = 2 #: A return to a previously sent method call 16 | ERROR = 3 #: A return to a method call that has failed 17 | SIGNAL = 4 #: A broadcast signal to subscribed connections 18 | 19 | 20 | MESSAGE_TYPE_MAP = {field.value: field for field in MessageType} 21 | 22 | 23 | class MessageFlag(IntFlag): 24 | """Flags that affect the behavior of sent and received messages 25 | """ 26 | NONE = 0 27 | NO_REPLY_EXPECTED = 1 #: The method call does not expect a method return. 28 | NO_AUTOSTART = 2 29 | ALLOW_INTERACTIVE_AUTHORIZATION = 4 30 | 31 | 32 | MESSAGE_FLAG_MAP = {field.value: field for field in MessageFlag} 33 | 34 | 35 | class NameFlag(IntFlag): 36 | """A flag that affects the behavior of a name request. 37 | """ 38 | NONE = 0 39 | ALLOW_REPLACEMENT = 1 #: If another client requests this name, let them have it. 40 | REPLACE_EXISTING = 2 #: If another client owns this name, try to take it. 41 | DO_NOT_QUEUE = 4 #: Name requests normally queue and wait for the owner to release the name. Do not enter this queue. 42 | 43 | 44 | class RequestNameReply(Enum): 45 | """An enum that describes the result of a name request. 46 | """ 47 | PRIMARY_OWNER = 1 #: The bus owns the name. 48 | IN_QUEUE = 2 #: The bus is in a queue and may receive the name after it is relased by the primary owner. 49 | EXISTS = 3 #: The name has an owner and NameFlag.DO_NOT_QUEUE was given. 50 | ALREADY_OWNER = 4 #: The bus already owns the name. 51 | 52 | 53 | class ReleaseNameReply(Enum): 54 | """An enum that describes the result of a name release request 55 | """ 56 | RELEASED = 1 57 | NON_EXISTENT = 2 58 | NOT_OWNER = 3 59 | 60 | 61 | class PropertyAccess(Enum): 62 | """An enum that describes whether a DBus property can be gotten or set with 63 | the ``org.freedesktop.DBus.Properties`` interface. 64 | """ 65 | READ = 'read' #: The property is readonly. 66 | WRITE = 'write' #: The property is writeonly. 67 | READWRITE = 'readwrite' #: The property can be read or written to. 68 | 69 | def readable(self): 70 | """Get whether the property can be read. 71 | """ 72 | return self == PropertyAccess.READ or self == PropertyAccess.READWRITE 73 | 74 | def writable(self): 75 | """Get whether the property can be written to. 76 | """ 77 | return self == PropertyAccess.WRITE or self == PropertyAccess.READWRITE 78 | 79 | 80 | class ArgDirection(Enum): 81 | """For an introspected argument, indicates whether it is an input parameter or a return value. 82 | """ 83 | IN = 'in' 84 | OUT = 'out' 85 | 86 | 87 | class ErrorType(Enum): 88 | """An enum for the type of an error for a message reply. 89 | 90 | :seealso: http://man7.org/linux/man-pages/man3/sd-bus-errors.3.html 91 | """ 92 | SERVICE_ERROR = 'com.dubstepdish.dbus.next.ServiceError' #: A custom error to indicate an exported service threw an exception. 93 | INTERNAL_ERROR = 'com.dubstepdish.dbus.next.InternalError' #: A custom error to indicate something went wrong with the library. 94 | CLIENT_ERROR = 'com.dubstepdish.dbus.next.ClientError' #: A custom error to indicate something went wrong with the client. 95 | 96 | FAILED = "org.freedesktop.DBus.Error.Failed" 97 | NO_MEMORY = "org.freedesktop.DBus.Error.NoMemory" 98 | SERVICE_UNKNOWN = "org.freedesktop.DBus.Error.ServiceUnknown" 99 | NAME_HAS_NO_OWNER = "org.freedesktop.DBus.Error.NameHasNoOwner" 100 | NO_REPLY = "org.freedesktop.DBus.Error.NoReply" 101 | IO_ERROR = "org.freedesktop.DBus.Error.IOError" 102 | BAD_ADDRESS = "org.freedesktop.DBus.Error.BadAddress" 103 | NOT_SUPPORTED = "org.freedesktop.DBus.Error.NotSupported" 104 | LIMITS_EXCEEDED = "org.freedesktop.DBus.Error.LimitsExceeded" 105 | ACCESS_DENIED = "org.freedesktop.DBus.Error.AccessDenied" 106 | AUTH_FAILED = "org.freedesktop.DBus.Error.AuthFailed" 107 | NO_SERVER = "org.freedesktop.DBus.Error.NoServer" 108 | TIMEOUT = "org.freedesktop.DBus.Error.Timeout" 109 | NO_NETWORK = "org.freedesktop.DBus.Error.NoNetwork" 110 | ADDRESS_IN_USE = "org.freedesktop.DBus.Error.AddressInUse" 111 | DISCONNECTED = "org.freedesktop.DBus.Error.Disconnected" 112 | INVALID_ARGS = "org.freedesktop.DBus.Error.InvalidArgs" 113 | FILE_NOT_FOUND = "org.freedesktop.DBus.Error.FileNotFound" 114 | FILE_EXISTS = "org.freedesktop.DBus.Error.FileExists" 115 | UNKNOWN_METHOD = "org.freedesktop.DBus.Error.UnknownMethod" 116 | UNKNOWN_OBJECT = "org.freedesktop.DBus.Error.UnknownObject" 117 | UNKNOWN_INTERFACE = "org.freedesktop.DBus.Error.UnknownInterface" 118 | UNKNOWN_PROPERTY = "org.freedesktop.DBus.Error.UnknownProperty" 119 | PROPERTY_READ_ONLY = "org.freedesktop.DBus.Error.PropertyReadOnly" 120 | UNIX_PROCESS_ID_UNKNOWN = "org.freedesktop.DBus.Error.UnixProcessIdUnknown" 121 | INVALID_SIGNATURE = "org.freedesktop.DBus.Error.InvalidSignature" 122 | INCONSISTENT_MESSAGE = "org.freedesktop.DBus.Error.InconsistentMessage" 123 | MATCH_RULE_NOT_FOUND = "org.freedesktop.DBus.Error.MatchRuleNotFound" 124 | MATCH_RULE_INVALID = "org.freedesktop.DBus.Error.MatchRuleInvalid" 125 | INTERACTIVE_AUTHORIZATION_REQUIRED = "org.freedesktop.DBus.Error.InteractiveAuthorizationRequired" 126 | -------------------------------------------------------------------------------- /dbus_next/errors.py: -------------------------------------------------------------------------------- 1 | class SignatureBodyMismatchError(ValueError): 2 | pass 3 | 4 | 5 | class InvalidSignatureError(ValueError): 6 | pass 7 | 8 | 9 | class InvalidAddressError(ValueError): 10 | pass 11 | 12 | 13 | class AuthError(Exception): 14 | pass 15 | 16 | 17 | class InvalidMessageError(ValueError): 18 | pass 19 | 20 | 21 | class InvalidIntrospectionError(ValueError): 22 | pass 23 | 24 | 25 | class InterfaceNotFoundError(Exception): 26 | pass 27 | 28 | 29 | class SignalDisabledError(Exception): 30 | pass 31 | 32 | 33 | class InvalidBusNameError(TypeError): 34 | def __init__(self, name): 35 | super().__init__(f'invalid bus name: {name}') 36 | 37 | 38 | class InvalidObjectPathError(TypeError): 39 | def __init__(self, path): 40 | super().__init__(f'invalid object path: {path}') 41 | 42 | 43 | class InvalidInterfaceNameError(TypeError): 44 | def __init__(self, name): 45 | super().__init__(f'invalid interface name: {name}') 46 | 47 | 48 | class InvalidMemberNameError(TypeError): 49 | def __init__(self, member): 50 | super().__init__(f'invalid member name: {member}') 51 | 52 | 53 | from .message import Message 54 | from .validators import assert_interface_name_valid 55 | from .constants import ErrorType, MessageType 56 | 57 | 58 | class DBusError(Exception): 59 | def __init__(self, type_, text, reply=None): 60 | super().__init__(text) 61 | 62 | if type(type_) is ErrorType: 63 | type_ = type_.value 64 | 65 | assert_interface_name_valid(type_) 66 | if reply is not None and type(reply) is not Message: 67 | raise TypeError('reply must be of type Message') 68 | 69 | self.type = type_ 70 | self.text = text 71 | self.reply = reply 72 | 73 | @staticmethod 74 | def _from_message(msg): 75 | assert msg.message_type == MessageType.ERROR 76 | return DBusError(msg.error_name, msg.body[0], reply=msg) 77 | 78 | def _as_message(self, msg): 79 | return Message.new_error(msg, self.type, self.text) 80 | -------------------------------------------------------------------------------- /dbus_next/glib/__init__.py: -------------------------------------------------------------------------------- 1 | from .message_bus import MessageBus 2 | from .proxy_object import ProxyObject, ProxyInterface 3 | -------------------------------------------------------------------------------- /dbus_next/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/altdesktop/python-dbus-next/ab566e16a71bfc9d7e0d29676aa459ec060e72c5/dbus_next/py.typed -------------------------------------------------------------------------------- /dbus_next/validators.py: -------------------------------------------------------------------------------- 1 | import re 2 | from .errors import InvalidBusNameError, InvalidObjectPathError, InvalidInterfaceNameError, InvalidMemberNameError 3 | from functools import lru_cache 4 | 5 | _bus_name_re = re.compile(r'^[A-Za-z_-][A-Za-z0-9_-]*$') 6 | _path_re = re.compile(r'^[A-Za-z0-9_]+$') 7 | _element_re = re.compile(r'^[A-Za-z_][A-Za-z0-9_]*$') 8 | _member_re = re.compile(r'^[A-Za-z_][A-Za-z0-9_-]*$') 9 | 10 | 11 | @lru_cache(maxsize=32) 12 | def is_bus_name_valid(name: str) -> bool: 13 | """Whether this is a valid bus name. 14 | 15 | .. seealso:: https://dbus.freedesktop.org/doc/dbus-specification.html#message-protocol-names-bus 16 | 17 | :param name: The bus name to validate. 18 | :type name: str 19 | 20 | :returns: Whether the name is a valid bus name. 21 | :rtype: bool 22 | """ 23 | if not isinstance(name, str): 24 | return False 25 | 26 | if not name or len(name) > 255: 27 | return False 28 | 29 | if name.startswith(':'): 30 | # a unique bus name 31 | return True 32 | 33 | if name.startswith('.'): 34 | return False 35 | 36 | if name.find('.') == -1: 37 | return False 38 | 39 | for element in name.split('.'): 40 | if _bus_name_re.search(element) is None: 41 | return False 42 | 43 | return True 44 | 45 | 46 | @lru_cache(maxsize=1024) 47 | def is_object_path_valid(path: str) -> bool: 48 | """Whether this is a valid object path. 49 | 50 | .. seealso:: https://dbus.freedesktop.org/doc/dbus-specification.html#message-protocol-marshaling-object-path 51 | 52 | :param path: The object path to validate. 53 | :type path: str 54 | 55 | :returns: Whether the object path is valid. 56 | :rtype: bool 57 | """ 58 | if not isinstance(path, str): 59 | return False 60 | 61 | if not path: 62 | return False 63 | 64 | if not path.startswith('/'): 65 | return False 66 | 67 | if len(path) == 1: 68 | return True 69 | 70 | for element in path[1:].split('/'): 71 | if _path_re.search(element) is None: 72 | return False 73 | 74 | return True 75 | 76 | 77 | @lru_cache(maxsize=32) 78 | def is_interface_name_valid(name: str) -> bool: 79 | """Whether this is a valid interface name. 80 | 81 | .. seealso:: https://dbus.freedesktop.org/doc/dbus-specification.html#message-protocol-names-interface 82 | 83 | :param name: The interface name to validate. 84 | :type name: str 85 | 86 | :returns: Whether the name is a valid interface name. 87 | :rtype: bool 88 | """ 89 | if not isinstance(name, str): 90 | return False 91 | 92 | if not name or len(name) > 255: 93 | return False 94 | 95 | if name.startswith('.'): 96 | return False 97 | 98 | if name.find('.') == -1: 99 | return False 100 | 101 | for element in name.split('.'): 102 | if _element_re.search(element) is None: 103 | return False 104 | 105 | return True 106 | 107 | 108 | @lru_cache(maxsize=512) 109 | def is_member_name_valid(member: str) -> bool: 110 | """Whether this is a valid member name. 111 | 112 | .. seealso:: https://dbus.freedesktop.org/doc/dbus-specification.html#message-protocol-names-member 113 | 114 | :param member: The member name to validate. 115 | :type member: str 116 | 117 | :returns: Whether the name is a valid member name. 118 | :rtype: bool 119 | """ 120 | if not isinstance(member, str): 121 | return False 122 | 123 | if not member or len(member) > 255: 124 | return False 125 | 126 | if _member_re.search(member) is None: 127 | return False 128 | 129 | return True 130 | 131 | 132 | def assert_bus_name_valid(name: str): 133 | """Raise an error if this is not a valid bus name. 134 | 135 | .. seealso:: https://dbus.freedesktop.org/doc/dbus-specification.html#message-protocol-names-bus 136 | 137 | :param name: The bus name to validate. 138 | :type name: str 139 | 140 | :raises: 141 | - :class:`InvalidBusNameError` - If this is not a valid bus name. 142 | """ 143 | if not is_bus_name_valid(name): 144 | raise InvalidBusNameError(name) 145 | 146 | 147 | def assert_object_path_valid(path: str): 148 | """Raise an error if this is not a valid object path. 149 | 150 | .. seealso:: https://dbus.freedesktop.org/doc/dbus-specification.html#message-protocol-marshaling-object-path 151 | 152 | :param path: The object path to validate. 153 | :type path: str 154 | 155 | :raises: 156 | - :class:`InvalidObjectPathError` - If this is not a valid object path. 157 | """ 158 | if not is_object_path_valid(path): 159 | raise InvalidObjectPathError(path) 160 | 161 | 162 | def assert_interface_name_valid(name: str): 163 | """Raise an error if this is not a valid interface name. 164 | 165 | .. seealso:: https://dbus.freedesktop.org/doc/dbus-specification.html#message-protocol-names-interface 166 | 167 | :param name: The interface name to validate. 168 | :type name: str 169 | 170 | :raises: 171 | - :class:`InvalidInterfaceNameError` - If this is not a valid object path. 172 | """ 173 | if not is_interface_name_valid(name): 174 | raise InvalidInterfaceNameError(name) 175 | 176 | 177 | def assert_member_name_valid(member): 178 | """Raise an error if this is not a valid member name. 179 | 180 | .. seealso:: https://dbus.freedesktop.org/doc/dbus-specification.html#message-protocol-names-member 181 | 182 | :param member: The member name to validate. 183 | :type member: str 184 | 185 | :raises: 186 | - :class:`InvalidMemberNameError` - If this is not a valid object path. 187 | """ 188 | if not is_member_name_valid(member): 189 | raise InvalidMemberNameError(member) 190 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SPHINXPROJ = dbus-next 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -------------------------------------------------------------------------------- /docs/_static/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/altdesktop/python-dbus-next/ab566e16a71bfc9d7e0d29676aa459ec060e72c5/docs/_static/.gitignore -------------------------------------------------------------------------------- /docs/_templates/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/altdesktop/python-dbus-next/ab566e16a71bfc9d7e0d29676aa459ec060e72c5/docs/_templates/.gitignore -------------------------------------------------------------------------------- /docs/authentication.rst: -------------------------------------------------------------------------------- 1 | Authentication 2 | ============== 3 | 4 | Classes for the DBus `authentication protocol `_ for us with :class:`MessageBus ` implementations. 5 | 6 | .. autoclass:: dbus_next.auth.Authenticator 7 | 8 | .. autoclass:: dbus_next.auth.AuthExternal 9 | .. autoclass:: dbus_next.auth.AuthAnnonymous 10 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Configuration file for the Sphinx documentation builder. 4 | # 5 | # This file does only contain a selection of the most common options. For a 6 | # full list see the documentation: 7 | # http://www.sphinx-doc.org/en/master/config 8 | 9 | # -- Path setup -------------------------------------------------------------- 10 | 11 | # If extensions (or modules to document with autodoc) are in another directory, 12 | # add these directories to sys.path here. If the directory is relative to the 13 | # documentation root, use os.path.abspath to make it absolute, like shown here. 14 | # 15 | import os 16 | import sys 17 | sys.path.insert(0, os.path.abspath(os.path.dirname(__file__) + '/..')) 18 | 19 | from dbus_next.__version__ import __title__, __author__, __version__, __copyright__ 20 | _project_slug = __title__.replace('_', '-') 21 | 22 | # -- Project information ----------------------------------------------------- 23 | 24 | project = _project_slug 25 | copyright = __copyright__ 26 | author = __author__ 27 | 28 | # The short X.Y version 29 | version = __version__ 30 | # The full version, including alpha/beta/rc tags 31 | release = __version__ 32 | 33 | # -- General configuration --------------------------------------------------- 34 | 35 | # If your documentation needs a minimal Sphinx version, state it here. 36 | # 37 | # needs_sphinx = '1.0' 38 | 39 | # Add any Sphinx extension module names here, as strings. They can be 40 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 41 | # ones. 42 | extensions = [ 43 | 'sphinx.ext.autodoc', 'sphinx.ext.githubpages', 'sphinxcontrib.asyncio', 'sphinxcontrib.fulltoc' 44 | ] 45 | 46 | # Add any paths that contain templates here, relative to this directory. 47 | templates_path = ['_templates'] 48 | 49 | # The suffix(es) of source filenames. 50 | # You can specify multiple suffix as a list of string: 51 | # 52 | # source_suffix = ['.rst', '.md'] 53 | source_suffix = '.rst' 54 | 55 | # The master toctree document. 56 | master_doc = 'index' 57 | 58 | # The language for content autogenerated by Sphinx. Refer to documentation 59 | # for a list of supported languages. 60 | # 61 | # This is also used if you do content translation via gettext catalogs. 62 | # Usually you set "language" from the command line for these cases. 63 | language = None 64 | 65 | # List of patterns, relative to source directory, that match files and 66 | # directories to ignore when looking for source files. 67 | # This pattern also affects html_static_path and html_extra_path . 68 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 69 | 70 | # The name of the Pygments (syntax highlighting) style to use. 71 | pygments_style = 'sphinx' 72 | 73 | # -- Options for HTML output ------------------------------------------------- 74 | 75 | # The theme to use for HTML and HTML Help pages. See the documentation for 76 | # a list of builtin themes. 77 | # 78 | html_theme = 'alabaster' 79 | 80 | # Theme options are theme-specific and customize the look and feel of a theme 81 | # further. For a list of options available for each theme, see the 82 | # documentation. 83 | # 84 | # html_theme_options = {} 85 | 86 | # Add any paths that contain custom static files (such as style sheets) here, 87 | # relative to this directory. They are copied after the builtin static files, 88 | # so a file named "default.css" will overwrite the builtin "default.css". 89 | html_static_path = ['_static'] 90 | 91 | # Custom sidebar templates, must be a dictionary that maps document names 92 | # to template names. 93 | # 94 | # The default sidebars (for documents that don't match any pattern) are 95 | # defined by theme itself. Builtin themes are using these templates by 96 | # default: ``['localtoc.html', 'relations.html', 'sourcelink.html', 97 | # 'searchbox.html']``. 98 | # 99 | # html_sidebars = {} 100 | 101 | # -- Options for HTMLHelp output --------------------------------------------- 102 | 103 | # Output file base name for HTML help builder. 104 | htmlhelp_basename = 'dbus-nextdoc' 105 | 106 | # -- Options for LaTeX output ------------------------------------------------ 107 | 108 | latex_elements = { 109 | # The paper size ('letterpaper' or 'a4paper'). 110 | # 111 | # 'papersize': 'letterpaper', 112 | 113 | # The font size ('10pt', '11pt' or '12pt'). 114 | # 115 | # 'pointsize': '10pt', 116 | 117 | # Additional stuff for the LaTeX preamble. 118 | # 119 | # 'preamble': '', 120 | 121 | # Latex figure (float) alignment 122 | # 123 | # 'figure_align': 'htbp', 124 | } 125 | 126 | # Grouping the document tree into LaTeX files. List of tuples 127 | # (source start file, target name, title, 128 | # author, documentclass [howto, manual, or own class]). 129 | latex_documents = [ 130 | (master_doc, 'dbus-next.tex', 'dbus-next Documentation', __author__, 'manual'), 131 | ] 132 | 133 | # -- Options for manual page output ------------------------------------------ 134 | 135 | # One entry per manual page. List of tuples 136 | # (source start file, name, description, authors, manual section). 137 | man_pages = [(master_doc, _project_slug, 'dbus-next Documentation', [author], 1)] 138 | 139 | # -- Options for Texinfo output ---------------------------------------------- 140 | 141 | # Grouping the document tree into Texinfo files. List of tuples 142 | # (source start file, target name, title, author, 143 | # dir menu entry, description, category) 144 | texinfo_documents = [ 145 | (master_doc, _project_slug, 'dbus-next Documentation', author, _project_slug, 146 | 'One line description of project.', 'Miscellaneous'), 147 | ] 148 | 149 | # -- Extension configuration ------------------------------------------------- 150 | -------------------------------------------------------------------------------- /docs/constants.rst: -------------------------------------------------------------------------------- 1 | Constants 2 | ========= 3 | 4 | 5 | .. autoclass:: dbus_next.BusType 6 | :members: 7 | :undoc-members: 8 | 9 | .. autoclass:: dbus_next.MessageType 10 | :members: 11 | :undoc-members: 12 | 13 | .. autoclass:: dbus_next.MessageFlag 14 | :members: 15 | :undoc-members: 16 | 17 | .. autoclass:: dbus_next.NameFlag 18 | :members: 19 | :undoc-members: 20 | 21 | .. autoclass:: dbus_next.RequestNameReply 22 | :members: 23 | :undoc-members: 24 | 25 | .. autoclass:: dbus_next.ReleaseNameReply 26 | :members: 27 | :undoc-members: 28 | 29 | .. autoclass:: dbus_next.PropertyAccess 30 | :members: 31 | :undoc-members: 32 | 33 | .. autoclass:: dbus_next.ArgDirection 34 | :members: 35 | :undoc-members: 36 | 37 | .. autoclass:: dbus_next.ErrorType 38 | :members: 39 | :undoc-members: 40 | -------------------------------------------------------------------------------- /docs/errors.rst: -------------------------------------------------------------------------------- 1 | Errors 2 | ====== 3 | 4 | .. autoclass:: dbus_next.DBusError 5 | :members: 6 | :undoc-members: 7 | 8 | .. autoclass:: dbus_next.SignatureBodyMismatchError 9 | .. autoclass:: dbus_next.InvalidSignatureError 10 | .. autoclass:: dbus_next.InvalidAddressError 11 | .. autoclass:: dbus_next.AuthError 12 | .. autoclass:: dbus_next.InvalidMessageError 13 | .. autoclass:: dbus_next.InvalidIntrospectionError 14 | .. autoclass:: dbus_next.InterfaceNotFoundError 15 | .. autoclass:: dbus_next.SignalDisabledError 16 | .. autoclass:: dbus_next.InvalidBusNameError 17 | .. autoclass:: dbus_next.InvalidObjectPathError 18 | .. autoclass:: dbus_next.InvalidInterfaceNameError 19 | .. autoclass:: dbus_next.InvalidMemberNameError 20 | -------------------------------------------------------------------------------- /docs/high-level-client/aio-proxy-interface.rst: -------------------------------------------------------------------------------- 1 | aio.ProxyInterface 2 | ================== 3 | 4 | .. autoclass:: dbus_next.aio.ProxyInterface 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/high-level-client/aio-proxy-object.rst: -------------------------------------------------------------------------------- 1 | aio.ProxyObject 2 | =============== 3 | 4 | .. autoclass:: dbus_next.aio.ProxyObject 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/high-level-client/base-proxy-interface.rst: -------------------------------------------------------------------------------- 1 | BaseProxyInterface 2 | ================== 3 | 4 | .. autoclass:: dbus_next.proxy_object.BaseProxyInterface 5 | :members: 6 | :undoc-members: 7 | -------------------------------------------------------------------------------- /docs/high-level-client/base-proxy-object.rst: -------------------------------------------------------------------------------- 1 | BaseProxyObject 2 | =============== 3 | 4 | .. autoclass:: dbus_next.proxy_object.BaseProxyObject 5 | :members: 6 | :undoc-members: 7 | -------------------------------------------------------------------------------- /docs/high-level-client/glib-proxy-interface.rst: -------------------------------------------------------------------------------- 1 | glib.ProxyInterface 2 | =================== 3 | 4 | .. autoclass:: dbus_next.glib.ProxyInterface 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/high-level-client/glib-proxy-object.rst: -------------------------------------------------------------------------------- 1 | glib.ProxyObject 2 | ================ 3 | 4 | .. autoclass:: dbus_next.glib.ProxyObject 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/high-level-client/index.rst: -------------------------------------------------------------------------------- 1 | The High Level Client 2 | ===================== 3 | 4 | .. toctree:: 5 | :maxdepth: 2 6 | 7 | base-proxy-object 8 | base-proxy-interface 9 | aio-proxy-object 10 | aio-proxy-interface 11 | glib-proxy-object 12 | glib-proxy-interface 13 | 14 | DBus interfaces are defined with an XML-based `introspection data format `_ which is exposed over the standard `org.freedesktop.DBus.Introspectable `_ interface. Calling the ``Introspect`` at a particular object path may return XML data similar to this: 15 | 16 | .. code-block:: xml 17 | 18 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | The object at this path (a ``node``) may contain interfaces and child nodes. Nodes like this are represented in the library by a :class:`ProxyObject `. The interfaces contained in the nodes are represented by a :class:`ProxyInterface `. The proxy interface exposes the methods, signals, and properties specified by the interface definition. 44 | 45 | The proxy object is obtained by the :class:`MessageBus ` through the :func:`get_proxy_object() ` method. This method takes the name of the client to send messages to, the path exported by that client that is expected to export the node, and the XML introspection data. If you can, it is recommended to include the XML in your project and pass it to that method as a string. But you may also use the :func:`introspect() ` method of the message bus to get this data dynamically at runtime. 46 | 47 | Once you have a proxy object, use the :func:`get_proxy_interface() ` method to create an interface passing the name of the interface to get. Each message bus has its own implementation of the proxy interface which behaves slightly differently. This is an example of how to use a proxy interface for the asyncio :class:`MessageBus `. 48 | 49 | If any file descriptors are sent or received (DBus type ``h``), the variable refers to the file descriptor itself. You are responsible for closing any file descriptors sent or received by the bus. You must set the ``negotiate_unix_fd`` flag to ``True`` in the ``MessageBus`` constructor to use unix file descriptors. 50 | 51 | :example: 52 | 53 | .. code-block:: python3 54 | 55 | from dbus_next.aio import MessageBus 56 | from dbus_next import Variant 57 | 58 | bus = await MessageBus().connect() 59 | 60 | with open('introspection.xml', 'r') as f: 61 | introspection = f.read() 62 | 63 | # alternatively, get the data dynamically: 64 | # introspection = await bus.introspect('com.example.name', 65 | # '/com/example/sample_object0') 66 | 67 | proxy_object = bus.get_proxy_object('com.example.name', 68 | '/com/example/sample_object0', 69 | introspection) 70 | 71 | interface = proxy_object.get_interface('com.example.SampleInterface0') 72 | 73 | # Use call_[METHOD] in snake case to call methods, passing the 74 | # in args and receiving the out args. The `baz` returned will 75 | # be type 'a{us}' which translates to a Python dict with `int` 76 | # keys and `str` values. 77 | baz = await interface.call_frobate(5, 'hello') 78 | 79 | # `bar` will be a Variant. 80 | bar = await interface.call_bazify([-5, 5, 5]) 81 | 82 | await interface.call_mogrify([5, 5, [ Variant('s', 'foo') ]) 83 | 84 | # Listen to signals by defining a callback that takes the args 85 | # specified by the signal definition and registering it on the 86 | # interface with on_[SIGNAL] in snake case. 87 | 88 | def changed_notify(new_value): 89 | print(f'The new value is: {new_value}') 90 | 91 | interface.on_changed(changed_notify) 92 | 93 | # Use get_[PROPERTY] and set_[PROPERTY] with the property in 94 | # snake case to get and set the property. 95 | 96 | bar_value = await interface.get_bar() 97 | 98 | await interface.set_bar(105) 99 | 100 | await bus.wait_for_disconnect() 101 | 102 | -------------------------------------------------------------------------------- /docs/high-level-service/index.rst: -------------------------------------------------------------------------------- 1 | The High Level Service 2 | ====================== 3 | 4 | .. toctree:: 5 | :maxdepth: 2 6 | 7 | service-interface 8 | 9 | The high level service interface provides everything you need to export interfaces on the bus. When you export an interface on your :class:`MessageBus `, clients can send you messages to call methods, get and set properties, and listen to your signals. 10 | 11 | If you're exposing a service for general use, you can request a well-known name for your connection with :func:`MessageBus.request_name() ` so users have a predictable name to use to send messages your client. 12 | 13 | Services are defined by subclassing :class:`ServiceInterface ` and definining members as methods on the class with the decorator methods :func:`@method() `, :func:`@dbus_property() `, and :func:`@signal() `. The parameters of the decorated class methods must be annotated with DBus type strings to indicate the types of values they expect. See the documentation on `the type system `_ for more information on how DBus types are mapped to Python values with signature strings. The decorator methods themselves take arguments that affect how the member is exported on the bus, such as the name of the member or the access permissions of a property. 14 | 15 | A class method decorated with ``@method()`` will be called when a client calls the method over DBus. The parameters given to the class method will be provided by the calling client and will conform to the parameter type annotations. The value returned by the class method will be returned to the client and must conform to the return type annotation specified by the user. If the return annotation specifies more than one type, the values must be returned in a ``list``. When :class:`aio.MessageBus` is used, methods can be coroutines. 16 | 17 | A class method decorated with ``@dbus_property()`` will be exposed as a DBus property getter. This decoration works the same as a standard Python ``@property``. The getter will be called when a client gets the property through the standard properties interface with ``org.freedesktop.DBus.Properties.Get``. Define a property setter with ``@method_name.setter`` taking the new value as a parameter. The setter will be called when the client sets the property through ``org.freedesktop.DBus.Properties.Set``. When :class:`aio.MessageBus` is used, property getters and setters can be coroutines, although this will cause some functionality of the Python ``@property`` annotation to be lost. 18 | 19 | A class method decorated with ``@signal()`` will be exposed as a DBus signal. The value returned by the class method will be emitted as a signal and broadcast to clients who are listening to the signal. The returned value must conform to the return annotation of the class method as a DBus signature string. If the signal has more than one argument, they must be returned within a ``list``. 20 | 21 | A class method decorated with ``@method()`` or ``@dbus_property()`` may throw a :class:`DBusError ` to return a detailed error to the client if something goes wrong. 22 | 23 | After the service interface is defined, call :func:`MessageBus.export() ` on a connected message bus and the service will be made available on the given object path. 24 | 25 | If any file descriptors are sent or received (DBus type ``h``), the variable refers to the file descriptor itself. You are responsible for closing any file descriptors sent or received by the bus. You must set the ``negotiate_unix_fd`` flag to ``True`` in the ``MessageBus`` constructor to use unix file descriptors. 26 | 27 | :example: 28 | 29 | .. code-block:: python3 30 | 31 | from dbus_next.aio import MessageBus 32 | from dbus_next.service import (ServiceInterface, 33 | method, dbus_property, signal) 34 | from dbus_next import Variant, DBusError 35 | 36 | import asyncio 37 | 38 | class ExampleInterface(ServiceInterface): 39 | def __init__(self): 40 | super().__init__('com.example.SampleInterface0') 41 | self._bar = 105 42 | 43 | @method() 44 | def Frobate(self, foo: 'i', bar: 's') -> 'a{us}': 45 | print(f'called Frobate with foo={foo} and bar={bar}') 46 | 47 | return { 48 | 1: 'one', 49 | 2: 'two' 50 | } 51 | 52 | @method() 53 | async def Bazify(self, bar: '(iiu)') -> 'vv': 54 | print(f'called Bazify with bar={bar}') 55 | 56 | return [Variant('s', 'example'), Variant('s', 'bazify')] 57 | 58 | @method() 59 | def Mogrify(self, bar: '(iiav)'): 60 | raise DBusError('com.example.error.CannotMogrify', 61 | 'it is not possible to mogrify') 62 | 63 | @signal() 64 | def Changed(self) -> 'b': 65 | return True 66 | 67 | @dbus_property() 68 | def Bar(self) -> 'y': 69 | return self._bar 70 | 71 | @Bar.setter 72 | def Bar(self, val: 'y'): 73 | if self._bar == val: 74 | return 75 | 76 | self._bar = val 77 | 78 | self.emit_properties_changed({'Bar': self._bar}) 79 | 80 | async def main(): 81 | bus = await MessageBus().connect() 82 | interface = ExampleInterface() 83 | bus.export('/com/example/sample0', interface) 84 | await bus.request_name('com.example.name') 85 | 86 | # emit the changed signal after two seconds. 87 | await asyncio.sleep(2) 88 | 89 | interface.Changed() 90 | 91 | await bus.wait_for_disconnect() 92 | 93 | asyncio.get_event_loop().run_until_complete(main()) 94 | -------------------------------------------------------------------------------- /docs/high-level-service/service-interface.rst: -------------------------------------------------------------------------------- 1 | ServiceInterface 2 | ================ 3 | 4 | .. autoclass:: dbus_next.service.ServiceInterface 5 | :members: 6 | :undoc-members: 7 | 8 | .. autodecorator:: dbus_next.service.dbus_property 9 | 10 | .. autodecorator:: dbus_next.service.method 11 | 12 | .. autodecorator:: dbus_next.service.signal 13 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Python DBus-Next Documentation 2 | ============================== 3 | 4 | .. module:: dbus_next 5 | 6 | .. toctree:: 7 | :maxdepth: 3 8 | :caption: Reference: 9 | 10 | type-system/index.rst 11 | high-level-client/index.rst 12 | high-level-service/index.rst 13 | low-level-interface/index.rst 14 | message-bus/index.rst 15 | introspection 16 | validators 17 | constants 18 | errors 19 | authentication 20 | 21 | Overview 22 | ++++++++ 23 | 24 | Python DBus-Next is a library for the `DBus message bus system `_ for interprocess communcation in a Linux desktop or mobile environment. 25 | 26 | Desktop application developers can use this library for integrating their applications into desktop environments by implementing common DBus standard interfaces or creating custom plugin interfaces. 27 | 28 | Desktop users can use this library to create their own scripts and utilities to interact with those interfaces for customization of their desktop environment. 29 | 30 | While other libraries for DBus exist for Python, this library offers the following improvements: 31 | 32 | - Zero dependencies and pure Python 3. 33 | - Support for multiple main loop backends including asyncio and the GLib main loop. 34 | - Nonblocking IO suitable for GUI development. 35 | - Target the latest language features of Python for beautiful services and clients. 36 | - Complete implementation of the DBus type system without ever guessing types. 37 | - Integration tests for all features of the library. 38 | - Completely documented public API. 39 | 40 | The library offers three core interfaces: 41 | 42 | - `The High Level Client `_ - Communicate with an existing interface exported on the bus by another client through a proxy object. 43 | - `The High Level Service `_ - Export a service interface for your application other clients can connect to for interaction with your application at runtime. 44 | - `The Low Level Interface `_ - Work with DBus messages directly for applications that work with the DBus daemon directly or to build your own high level abstractions. 45 | 46 | Installation 47 | ++++++++++++ 48 | 49 | This library is available on PyPi as `dbus-next `_. 50 | 51 | .. code-block:: bash 52 | 53 | pip3 install dbus-next 54 | 55 | Contributing 56 | ++++++++++++ 57 | 58 | Development for this library happens on `Github `_. Report bugs or request features there. Contributions are welcome. 59 | 60 | License 61 | ++++++++ 62 | 63 | This library is available under an `MIT License `_. 64 | 65 | © 2019, Tony Crisci 66 | 67 | Indices and tables 68 | ================== 69 | 70 | * :ref:`genindex` 71 | * :ref:`modindex` 72 | * :ref:`search` 73 | -------------------------------------------------------------------------------- /docs/introspection.rst: -------------------------------------------------------------------------------- 1 | Introspection 2 | ============= 3 | 4 | .. autoclass:: dbus_next.introspection.Node 5 | :members: 6 | :undoc-members: 7 | 8 | .. autoclass:: dbus_next.introspection.Interface 9 | :members: 10 | :undoc-members: 11 | 12 | .. autoclass:: dbus_next.introspection.Property 13 | :members: 14 | :undoc-members: 15 | 16 | .. autoclass:: dbus_next.introspection.Method 17 | :members: 18 | :undoc-members: 19 | 20 | .. autoclass:: dbus_next.introspection.Signal 21 | :members: 22 | :undoc-members: 23 | 24 | .. autoclass:: dbus_next.introspection.Arg 25 | :members: 26 | :undoc-members: 27 | -------------------------------------------------------------------------------- /docs/low-level-interface/index.rst: -------------------------------------------------------------------------------- 1 | The Low Level Interface 2 | ======================= 3 | 4 | .. toctree:: 5 | :maxdepth: 2 6 | 7 | message 8 | 9 | The low-level interface allows you to work with messages directly through the :class:`MessageBus ` with the :class:`Message ` class. This might be useful in the following cases: 10 | 11 | - Implementing an application that works with DBus directly like ``dbus-send(1)`` or ``dbus-monitor(1)``. 12 | - Creating a new implementation of the :class:`BaseMessageBus `. 13 | - Creating clients or services that use an alternative to the standard DBus interfaces. 14 | 15 | The primary methods and classes of the low-level interface are: 16 | 17 | - :class:`Message ` 18 | - :func:`MessageBus.send() ` 19 | - :func:`MessageBus.add_message_handler() ` 20 | - :func:`MessageBus.remove_message_handler() ` 21 | - :func:`MessageBus.next_serial() ` 22 | - :func:`aio.MessageBus.call() ` 23 | - :func:`glib.MessageBus.call() ` 24 | - :func:`glib.MessageBus.call_sync() ` 25 | 26 | Mixed use of the low and high level interfaces on the same bus connection is not recommended. 27 | 28 | :example: Call a standard interface 29 | 30 | .. code-block:: python3 31 | 32 | bus = await MessageBus().connect() 33 | 34 | msg = Message(destination='org.freedesktop.DBus', 35 | path='/org/freedesktop/DBus', 36 | interface='org.freedesktop.DBus', 37 | member='ListNames', 38 | serial=bus.next_serial()) 39 | 40 | reply = await bus.call(msg) 41 | 42 | assert reply.message_type == MessageType.METHOD_RETURN 43 | 44 | print(reply.body[0]) 45 | 46 | :example: A custom method handler. Note that to receive these messages, you must `add a match rule `_ for the types of messages you want to receive. 47 | 48 | .. code-block:: python3 49 | 50 | bus = await MessageBus().connect() 51 | 52 | reply = await bus.call( 53 | Message(destination='org.freedesktop.DBus', 54 | path='/org/freedesktop/DBus', 55 | member='AddMatch', 56 | signature='s', 57 | body=["member='MyMember', interface='com.test.interface'"])) 58 | 59 | assert reply.message_type == MessageType.METHOD_RETURN 60 | 61 | def message_handler(msg): 62 | if msg.interface == 'com.test.interface' and msg.member == 'MyMember': 63 | return Message.new_method_return(msg, 's', ['got it']) 64 | 65 | bus.add_message_handler(message_handler) 66 | 67 | await bus.wait_for_disconnect() 68 | 69 | :example: Emit a signal 70 | 71 | .. code-block:: python3 72 | 73 | bus = await MessageBus().connect() 74 | 75 | await bus.send(Message.new_signal('/com/test/path', 76 | 'com.test.interface', 77 | 'SomeSignal', 78 | 's', ['a signal'])) 79 | 80 | :example: Send a file descriptor. The message format will be the same when 81 | received on the client side. You are responsible for closing any file 82 | descriptor that is sent or received by the bus. You must set the 83 | ``negotiate_unix_fd`` flag to ``True`` in the ``MessageBus`` 84 | constructor to use unix file descriptors. 85 | 86 | .. code-block:: python3 87 | 88 | bus = await MessageBus().connect(negotiate_unix_fd=True) 89 | 90 | fd = os.open('/dev/null', os.O_RDONLY) 91 | 92 | msg = Message(destination='org.test.destination', 93 | path='/org/test/destination', 94 | interface='org.test.interface', 95 | member='TestMember', 96 | signature='h', 97 | body=[0], 98 | unix_fds=[fd]) 99 | 100 | await bus.send(msg) 101 | -------------------------------------------------------------------------------- /docs/low-level-interface/message.rst: -------------------------------------------------------------------------------- 1 | Message 2 | ======= 3 | 4 | .. autoclass:: dbus_next.Message 5 | :members: 6 | :undoc-members: 7 | -------------------------------------------------------------------------------- /docs/message-bus/aio-message-bus.rst: -------------------------------------------------------------------------------- 1 | aio.MessageBus 2 | ============== 3 | 4 | .. autoclass:: dbus_next.aio.MessageBus 5 | :members: 6 | :inherited-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/message-bus/base-message-bus.rst: -------------------------------------------------------------------------------- 1 | BaseMessageBus 2 | ============== 3 | 4 | .. autoclass:: dbus_next.message_bus.BaseMessageBus 5 | :members: 6 | -------------------------------------------------------------------------------- /docs/message-bus/glib-message-bus.rst: -------------------------------------------------------------------------------- 1 | glib.MessageBus 2 | =============== 3 | 4 | .. autoclass:: dbus_next.glib.MessageBus 5 | :members: 6 | :inherited-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/message-bus/index.rst: -------------------------------------------------------------------------------- 1 | The Message Bus 2 | =============== 3 | 4 | .. toctree:: 5 | :maxdepth: 2 6 | 7 | base-message-bus.rst 8 | aio-message-bus.rst 9 | glib-message-bus.rst 10 | 11 | The message bus manages a connection to the DBus daemon. It's capable of sending and receiving messages and wiring up the classes of the high level interfaces. 12 | 13 | There are currently two implementations of the message bus depending on what main loop implementation you want to use. Use :class:`aio.MessageBus ` if you are using an asyncio main loop. Use :class:`glib.MessageBus ` if you are using a GLib main loop. 14 | 15 | For standalone applications, the asyncio message bus is preferable because it has a nice async/await api in place of the callback/synchronous interface of the GLib message bus. If your application is using other libraries that use the GLib main loop, such as a GTK application, the GLib implementation will be needed. However neither library is a requirement. 16 | 17 | For more information on how to use the message bus, see the documentation for the specific interfaces you plan to use. 18 | -------------------------------------------------------------------------------- /docs/type-system/index.rst: -------------------------------------------------------------------------------- 1 | The Type System 2 | =============== 3 | 4 | .. toctree:: 5 | :maxdepth: 2 6 | 7 | variant 8 | signature-tree 9 | signature-type 10 | 11 | Values that are sent or received over the message bus always have an 12 | associated signature that specifies the types of those values. For the 13 | high-level client and service, these signatures are specified in XML 14 | data which is advertised in a `standard DBus 15 | interface `__. 16 | The high-level client dynamically creates classes based on this 17 | introspection data with methods and signals with arguments based on the 18 | type signature. The high-level service does the inverse by introspecting 19 | the class to create the introspection XML data which is advertised on 20 | the bus for clients. 21 | 22 | Each token in the signature is mapped to a Python type as shown in the table 23 | below. 24 | 25 | +-------------+-------+--------------------------------------+-------------------------------------------------------------------------+ 26 | | Name | Token | Python | Notes | 27 | | | | Type | | 28 | +-------------+-------+--------------------------------------+-------------------------------------------------------------------------+ 29 | | BYTE | y | int | An integer 0-255. In an array, it has type ``bytes``. | 30 | +-------------+-------+--------------------------------------+-------------------------------------------------------------------------+ 31 | | BOOLEAN | b | bool | | 32 | +-------------+-------+--------------------------------------+-------------------------------------------------------------------------+ 33 | | INT16 | n | int | | 34 | +-------------+-------+--------------------------------------+-------------------------------------------------------------------------+ 35 | | UINT16 | q | int | | 36 | +-------------+-------+--------------------------------------+-------------------------------------------------------------------------+ 37 | | INT32 | i | int | | 38 | +-------------+-------+--------------------------------------+-------------------------------------------------------------------------+ 39 | | UINT32 | u | int | | 40 | +-------------+-------+--------------------------------------+-------------------------------------------------------------------------+ 41 | | INT64 | x | int | | 42 | +-------------+-------+--------------------------------------+-------------------------------------------------------------------------+ 43 | | UINT64 | t | int | | 44 | +-------------+-------+--------------------------------------+-------------------------------------------------------------------------+ 45 | | DOUBLE | d | float | | 46 | +-------------+-------+--------------------------------------+-------------------------------------------------------------------------+ 47 | | STRING | s | str | | 48 | +-------------+-------+--------------------------------------+-------------------------------------------------------------------------+ 49 | | OBJECT_PATH | o | str | Must be a valid object path. | 50 | +-------------+-------+--------------------------------------+-------------------------------------------------------------------------+ 51 | | SIGNATURE | g | str | Must be a valid signature. | 52 | +-------------+-------+--------------------------------------+-------------------------------------------------------------------------+ 53 | | UNIX_FD | h | int | In the low-level interface, an index pointing to a file descriptor | 54 | | | | | in the ``unix_fds`` member of the :class:`Message `. | 55 | | | | | In the high-level interface, it is the file descriptor itself. | 56 | +-------------+-------+--------------------------------------+-------------------------------------------------------------------------+ 57 | | ARRAY | a | list | Must be followed by a complete type which specifies the child type. | 58 | +-------------+-------+--------------------------------------+-------------------------------------------------------------------------+ 59 | | STRUCT | ( | list | Types in the Python ``list`` must match the types between the parens. | 60 | +-------------+-------+--------------------------------------+-------------------------------------------------------------------------+ 61 | | VARIANT | v | :class:`Variant ` | This class is provided by the library. | 62 | | | | | | 63 | +-------------+-------+--------------------------------------+-------------------------------------------------------------------------+ 64 | | DICT_ENTRY | { | dict | Must be included in an array to be a ``dict``. | 65 | +-------------+-------+--------------------------------------+-------------------------------------------------------------------------+ 66 | 67 | The types ``a``, ``(``, ``v``, and ``{`` are container types that hold 68 | other values. Examples of container types and Python examples are in the 69 | table below. 70 | 71 | +-----------+--------------------------------------+-------------------------------------------------------+ 72 | | Signature | Example | Notes | 73 | +===========+======================================+=======================================================+ 74 | | ``(su)`` | ``[ 'foo', 5 ]`` | Each element in the array must match the | 75 | | | | corresponding type of the struct member. | 76 | +-----------+--------------------------------------+-------------------------------------------------------+ 77 | | ``as`` | ``[ 'foo', 'bar' ]`` | The child type comes immediately after the ``a``. | 78 | | | | The array can have any number of elements, but | 79 | | | | they all must match the child type. | 80 | +-----------+--------------------------------------+-------------------------------------------------------+ 81 | | ``a{su}`` | ``{ 'foo': 5 }`` | An "array of dict entries" is represented by a | 82 | | | | ``dict``. The type after ``{`` is the key type and | 83 | | | | the type before the ``}`` is the value type. | 84 | +-----------+--------------------------------------+-------------------------------------------------------+ 85 | | ``ay`` | ``b'\0x62\0x75\0x66'`` | Special case: an array of bytes is represented by | 86 | | | | Python ``bytes``. | 87 | | | | | 88 | | | | | 89 | | | | | 90 | | | | | 91 | +-----------+--------------------------------------+-------------------------------------------------------+ 92 | | ``v`` | ``Variant('as', ['hello'])`` | Signature must be a single type. A variant may hold a | 93 | | | | container type. | 94 | | | | | 95 | | | | | 96 | | | | | 97 | +-----------+--------------------------------------+-------------------------------------------------------+ 98 | | ``(asv)`` | ``[ ['foo'], Variant('s', 'bar') ]`` | Containers may be nested. | 99 | +-----------+--------------------------------------+-------------------------------------------------------+ 100 | 101 | For more information on the DBus type system, see `the 102 | specification `__. 103 | -------------------------------------------------------------------------------- /docs/type-system/signature-tree.rst: -------------------------------------------------------------------------------- 1 | SignatureTree 2 | ============= 3 | 4 | .. autoclass:: dbus_next.SignatureTree 5 | :members: 6 | :undoc-members: 7 | 8 | -------------------------------------------------------------------------------- /docs/type-system/signature-type.rst: -------------------------------------------------------------------------------- 1 | SignatureType 2 | ============== 3 | 4 | .. autoclass:: dbus_next.SignatureType 5 | :members: 6 | :undoc-members: 7 | :exclude-members: signature 8 | 9 | -------------------------------------------------------------------------------- /docs/type-system/variant.rst: -------------------------------------------------------------------------------- 1 | Variant 2 | ======= 3 | 4 | .. autoclass:: dbus_next.Variant 5 | :members: 6 | :undoc-members: 7 | -------------------------------------------------------------------------------- /docs/validators.rst: -------------------------------------------------------------------------------- 1 | Validators 2 | ========== 3 | 4 | .. autofunction:: dbus_next.is_bus_name_valid 5 | .. autofunction:: dbus_next.is_member_name_valid 6 | .. autofunction:: dbus_next.is_object_path_valid 7 | .. autofunction:: dbus_next.is_interface_name_valid 8 | .. autofunction:: dbus_next.assert_bus_name_valid 9 | .. autofunction:: dbus_next.assert_member_name_valid 10 | .. autofunction:: dbus_next.assert_object_path_valid 11 | .. autofunction:: dbus_next.assert_interface_name_valid 12 | -------------------------------------------------------------------------------- /examples/aio-list-names.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import sys 3 | import os 4 | 5 | sys.path.append(os.path.abspath(os.path.dirname(__file__) + '/..')) 6 | 7 | from dbus_next import Message, MessageType 8 | from dbus_next.aio import MessageBus 9 | 10 | import asyncio 11 | import json 12 | 13 | loop = asyncio.get_event_loop() 14 | 15 | 16 | async def main(): 17 | bus = await MessageBus().connect() 18 | 19 | reply = await bus.call( 20 | Message(destination='org.freedesktop.DBus', 21 | path='/org/freedesktop/DBus', 22 | interface='org.freedesktop.DBus', 23 | member='ListNames')) 24 | 25 | if reply.message_type == MessageType.ERROR: 26 | raise Exception(reply.body[0]) 27 | 28 | print(json.dumps(reply.body[0], indent=2)) 29 | 30 | 31 | loop.run_until_complete(main()) 32 | -------------------------------------------------------------------------------- /examples/aio-tcp-notification.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # In order for this to work a local tcp connection to the DBus a port 4 | # must be opened to forward to the dbus socket file. The easiest way 5 | # to achieve this is using "socat": 6 | # socat TCP-LISTEN:55556,reuseaddr,fork,range=127.0.0.1/32 UNIX-CONNECT:$(echo $DBUS_SESSION_BUS_ADDRESS | sed 's/unix:path=//g') 7 | # For actual DBus transport over network the authentication might 8 | # be a further problem. More information here: 9 | # https://dbus.freedesktop.org/doc/dbus-specification.html#auth-mechanisms 10 | 11 | import sys 12 | import os 13 | 14 | sys.path.append(os.path.abspath(os.path.dirname(__file__) + '/..')) 15 | 16 | from dbus_next.aio import MessageBus 17 | 18 | import asyncio 19 | 20 | loop = asyncio.get_event_loop() 21 | 22 | 23 | async def main(): 24 | bus = await MessageBus(bus_address="tcp:host=127.0.0.1,port=55556").connect() 25 | introspection = await bus.introspect('org.freedesktop.Notifications', 26 | '/org/freedesktop/Notifications') 27 | obj = bus.get_proxy_object('org.freedesktop.Notifications', '/org/freedesktop/Notifications', 28 | introspection) 29 | notification = obj.get_interface('org.freedesktop.Notifications') 30 | await notification.call_notify("test.py", 0, "", "DBus Test", "Test notification", [""], dict(), 31 | 5000) 32 | 33 | 34 | loop.run_until_complete(main()) 35 | -------------------------------------------------------------------------------- /examples/dbus-next-send.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import sys 3 | import os 4 | 5 | sys.path.append(os.path.abspath(os.path.dirname(__file__) + '/..')) 6 | 7 | from dbus_next.validators import (is_bus_name_valid, is_member_name_valid, is_object_path_valid, 8 | is_interface_name_valid) 9 | from dbus_next.aio import MessageBus 10 | from dbus_next import MessageType, BusType, Message, Variant 11 | from argparse import ArgumentParser, OPTIONAL 12 | import json 13 | 14 | import asyncio 15 | 16 | parser = ArgumentParser() 17 | 18 | parser.add_argument('--system', help='Use the system bus', action='store_true') 19 | parser.add_argument('--session', help='Use the session bus', action='store_true') 20 | parser.add_argument('--dest', help='The destination address for the message', required=True) 21 | parser.add_argument('--signature', help='The signature for the message body') 22 | parser.add_argument('--type', 23 | help='The type of message to send', 24 | choices=[e.name for e in MessageType], 25 | default=MessageType.METHOD_CALL.name, 26 | nargs=OPTIONAL) 27 | parser.add_argument('object_path', help='The object path for the message') 28 | parser.add_argument('interface.member', help='The interface and member for the message') 29 | parser.add_argument('body', 30 | help='The JSON encoded body of the message. Must match the signature', 31 | nargs=OPTIONAL) 32 | 33 | args = parser.parse_args() 34 | 35 | 36 | def exit_error(message): 37 | parser.print_usage() 38 | print() 39 | print(message) 40 | sys.exit(1) 41 | 42 | 43 | interface_member = vars(args)['interface.member'].split('.') 44 | 45 | if len(interface_member) < 2: 46 | exit_error( 47 | f'Expecting an interface and member separated by a dot: {vars(args)["interface.member"]}') 48 | 49 | destination = args.dest 50 | member = interface_member[-1] 51 | interface = '.'.join(interface_member[:len(interface_member) - 1]) 52 | object_path = args.object_path 53 | signature = args.signature 54 | body = args.body 55 | message_type = MessageType[args.type] 56 | signature = args.signature 57 | 58 | bus_type = BusType.SESSION 59 | 60 | if args.system: 61 | bus_type = BusType.SYSTEM 62 | 63 | if message_type is not MessageType.METHOD_CALL: 64 | exit_error('only message type METHOD_CALL is supported right now') 65 | 66 | if not is_bus_name_valid(destination): 67 | exit_error(f'got invalid bus name: {destination}') 68 | 69 | if not is_object_path_valid(object_path): 70 | exit_error(f'got invalid object path: {object_path}') 71 | 72 | if not is_interface_name_valid(interface): 73 | exit_error(f'got invalid interface name: {interface}') 74 | 75 | if not is_member_name_valid(member): 76 | exit_error(f'got invalid member name: {member}') 77 | 78 | if body is None: 79 | body = [] 80 | signature = '' 81 | else: 82 | try: 83 | body = json.loads(body) 84 | except json.JSONDecodeError as e: 85 | exit_error(f'could not parse body as JSON: ({e})') 86 | 87 | if type(body) is not list: 88 | exit_error('body must be an array of arguments') 89 | 90 | if not signature: 91 | exit_error('--signature is a required argument when passing a message body') 92 | 93 | loop = asyncio.get_event_loop() 94 | 95 | 96 | async def main(): 97 | bus = await MessageBus(bus_type=bus_type).connect() 98 | 99 | message = Message(destination=destination, 100 | member=member, 101 | interface=interface, 102 | path=object_path, 103 | signature=signature, 104 | body=body) 105 | 106 | result = await bus.call(message) 107 | 108 | ret = 0 109 | 110 | if result.message_type is MessageType.ERROR: 111 | print(f'Error: {result.error_name}', file=sys.stderr) 112 | ret = 1 113 | 114 | def default(o): 115 | if type(o) is Variant: 116 | return [o.signature, o.value] 117 | else: 118 | raise json.JSONDecodeError() 119 | 120 | print(json.dumps(result.body, indent=2, default=default)) 121 | 122 | sys.exit(ret) 123 | 124 | 125 | loop.run_until_complete(main()) 126 | -------------------------------------------------------------------------------- /examples/example-service.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import sys 3 | import os 4 | 5 | sys.path.append(os.path.abspath(os.path.dirname(__file__) + '/..')) 6 | 7 | from dbus_next.service import ServiceInterface, method, signal, dbus_property 8 | from dbus_next.aio.message_bus import MessageBus 9 | from dbus_next import Variant 10 | 11 | import asyncio 12 | 13 | 14 | class ExampleInterface(ServiceInterface): 15 | def __init__(self, name): 16 | super().__init__(name) 17 | self._string_prop = 'kevin' 18 | 19 | @method() 20 | def Echo(self, what: 's') -> 's': 21 | return what 22 | 23 | @method() 24 | def EchoMultiple(self, what1: 's', what2: 's') -> 'ss': 25 | return [what1, what2] 26 | 27 | @method() 28 | def GetVariantDict(self) -> 'a{sv}': 29 | return { 30 | 'foo': Variant('s', 'bar'), 31 | 'bat': Variant('x', -55), 32 | 'a_list': Variant('as', ['hello', 'world']) 33 | } 34 | 35 | @dbus_property(name='StringProp') 36 | def string_prop(self) -> 's': 37 | return self._string_prop 38 | 39 | @string_prop.setter 40 | def string_prop_setter(self, val: 's'): 41 | self._string_prop = val 42 | 43 | @signal() 44 | def signal_simple(self) -> 's': 45 | return 'hello' 46 | 47 | @signal() 48 | def signal_multiple(self) -> 'ss': 49 | return ['hello', 'world'] 50 | 51 | 52 | async def main(): 53 | name = 'dbus.next.example.service' 54 | path = '/example/path' 55 | interface_name = 'example.interface' 56 | 57 | bus = await MessageBus().connect() 58 | interface = ExampleInterface(interface_name) 59 | bus.export('/example/path', interface) 60 | await bus.request_name(name) 61 | print(f'service up on name: "{name}", path: "{path}", interface: "{interface_name}"') 62 | await bus.wait_for_disconnect() 63 | 64 | 65 | asyncio.get_event_loop().run_until_complete(main()) 66 | -------------------------------------------------------------------------------- /examples/glib-list-names.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import sys 3 | import os 4 | 5 | sys.path.append(os.path.abspath(os.path.dirname(__file__) + '/..')) 6 | 7 | from dbus_next import Message 8 | from dbus_next.glib import MessageBus 9 | 10 | import json 11 | import signal 12 | from gi.repository import GLib 13 | 14 | main = GLib.MainLoop() 15 | bus = MessageBus().connect_sync() 16 | 17 | 18 | def reply_handler(reply, err): 19 | main.quit() 20 | 21 | if err: 22 | raise err 23 | 24 | print(json.dumps(reply.body[0], indent=2)) 25 | 26 | 27 | bus.call( 28 | Message('org.freedesktop.DBus', '/org/freedesktop/DBus', 'org.freedesktop.DBus', 'ListNames'), 29 | reply_handler) 30 | 31 | signal.signal(signal.SIGINT, signal.SIG_DFL) 32 | main.run() 33 | -------------------------------------------------------------------------------- /examples/mpris.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import sys 3 | import os 4 | 5 | sys.path.append(os.path.abspath(os.path.dirname(__file__) + '/..')) 6 | 7 | from dbus_next.aio import MessageBus 8 | 9 | import asyncio 10 | 11 | loop = asyncio.get_event_loop() 12 | 13 | 14 | async def main(): 15 | bus = await MessageBus().connect() 16 | # the introspection xml would normally be included in your project, but 17 | # this is convenient for development 18 | introspection = await bus.introspect('org.mpris.MediaPlayer2.vlc', '/org/mpris/MediaPlayer2') 19 | 20 | obj = bus.get_proxy_object('org.mpris.MediaPlayer2.vlc', '/org/mpris/MediaPlayer2', 21 | introspection) 22 | player = obj.get_interface('org.mpris.MediaPlayer2.Player') 23 | properties = obj.get_interface('org.freedesktop.DBus.Properties') 24 | 25 | # call methods on the interface (this causes the media player to play) 26 | await player.call_play() 27 | 28 | volume = await player.get_volume() 29 | print(f'current volume: {volume}, setting to 0.5') 30 | 31 | await player.set_volume(0.5) 32 | 33 | # listen to signals 34 | def on_properties_changed(interface_name, changed_properties, invalidated_properties): 35 | for changed, variant in changed_properties.items(): 36 | print(f'property changed: {changed} - {variant.value}') 37 | 38 | properties.on_properties_changed(on_properties_changed) 39 | 40 | await bus.wait_for_disconnect() 41 | 42 | 43 | loop.run_until_complete(main()) 44 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | timeout = 5 3 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | pytest 2 | pytest-asyncio 3 | pytest-timeout 4 | pytest-cov 5 | yapf 6 | flake8 7 | sphinx 8 | sphinx-autobuild 9 | sphinxcontrib-asyncio 10 | sphinxcontrib-fulltoc 11 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # This library has no required dependencies. These dependencies are only for 2 | # running the test suite and documentation generator. 3 | 4 | pytest==6.2.2 5 | pytest-asyncio==0.14.0 6 | pytest-timeout==1.4.2 7 | pytest-cov==2.11.1 8 | yapf==0.31.0 9 | flake8==3.9.0 10 | Sphinx==3.5.3 11 | sphinx-autobuild==2021.3.14 12 | sphinxcontrib-asyncio==0.3.0 13 | sphinxcontrib-fulltoc==1.2.0 14 | ## The following requirements were added by pip freeze: 15 | alabaster==0.7.12 16 | attrs==20.3.0 17 | Babel==2.9.1 18 | certifi==2020.12.5 19 | chardet==4.0.0 20 | colorama==0.4.4 21 | coverage==5.5 22 | docutils==0.16 23 | idna==2.10 24 | imagesize==1.2.0 25 | iniconfig==1.1.1 26 | Jinja2==2.11.3 27 | livereload==2.6.3 28 | MarkupSafe==1.1.1 29 | mccabe==0.6.1 30 | packaging==20.9 31 | pluggy==0.13.1 32 | py==1.10.0 33 | pycodestyle==2.7.0 34 | pyflakes==2.3.0 35 | Pygments==2.8.1 36 | pyparsing==2.4.7 37 | pytz==2021.1 38 | requests==2.25.1 39 | six==1.15.0 40 | snowballstemmer==2.1.0 41 | sphinxcontrib-applehelp==1.0.2 42 | sphinxcontrib-devhelp==1.0.2 43 | sphinxcontrib-htmlhelp==1.0.3 44 | sphinxcontrib-jsmath==1.0.1 45 | sphinxcontrib-qthelp==1.0.3 46 | sphinxcontrib-serializinghtml==1.1.4 47 | toml==0.10.2 48 | tornado==6.1 49 | urllib3==1.26.5 50 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import io 5 | import os 6 | 7 | from setuptools import setup, find_packages 8 | 9 | # Package meta-data. 10 | DESCRIPTION = 'A zero-dependency DBus library for Python with asyncio support' 11 | REQUIRES_PYTHON = '>=3.6.0' 12 | 13 | # What packages are required for this module to be executed? 14 | REQUIRED = [] 15 | 16 | # What packages are optional? 17 | EXTRAS = {} 18 | 19 | # The rest you shouldn't have to touch too much :) 20 | # ------------------------------------------------ 21 | # Except, perhaps the License and Trove Classifiers! 22 | # If you do change the License, remember to change the Trove Classifier for that! 23 | 24 | here = os.path.abspath(os.path.dirname(__file__)) 25 | 26 | about = {} 27 | with open(os.path.join(here, 'dbus_next', '__version__.py')) as f: 28 | exec(f.read(), about) 29 | 30 | with io.open(os.path.join(here, 'README.md'), encoding='utf-8') as f: 31 | long_description = '\n' + f.read() 32 | 33 | setup( 34 | name=about['__title__'], 35 | version=about['__version__'], 36 | description=DESCRIPTION, 37 | long_description=long_description, 38 | long_description_content_type='text/markdown', 39 | author=about['__author__'], 40 | author_email=about['__author_email__'], 41 | python_requires=REQUIRES_PYTHON, 42 | url=about['__url__'], 43 | packages=find_packages(exclude=['test', '*.test', '*.test.*', 'test.*']), 44 | install_requires=REQUIRED, 45 | extras_require=EXTRAS, 46 | include_package_data=True, 47 | license='MIT', 48 | classifiers=[ 49 | # Full list: https://pypi.python.org/pypi?%3Aaction=list_classifiers 50 | 'Development Status :: 3 - Alpha', 51 | 'Environment :: X11 Applications', 52 | 'Environment :: X11 Applications :: Gnome', 53 | 'Topic :: Desktop Environment :: Gnome', 54 | 'Topic :: Software Development :: Embedded Systems', 55 | 'Framework :: AsyncIO', 56 | 'License :: OSI Approved :: MIT License', 57 | 'Programming Language :: Python', 58 | 'Programming Language :: Python :: 3', 59 | 'Programming Language :: Python :: 3.6', 60 | 'Programming Language :: Python :: 3.7', 61 | 'Programming Language :: Python :: Implementation :: CPython', 62 | 'Programming Language :: Python :: Implementation :: PyPy', 63 | 'Typing :: Typed' 64 | ]) 65 | -------------------------------------------------------------------------------- /test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/altdesktop/python-dbus-next/ab566e16a71bfc9d7e0d29676aa459ec060e72c5/test/__init__.py -------------------------------------------------------------------------------- /test/client/test_methods.py: -------------------------------------------------------------------------------- 1 | from dbus_next.message import MessageFlag 2 | from dbus_next.service import ServiceInterface, method 3 | import dbus_next.introspection as intr 4 | from dbus_next import aio, glib, DBusError 5 | from test.util import check_gi_repository, skip_reason_no_gi 6 | 7 | import pytest 8 | 9 | has_gi = check_gi_repository() 10 | 11 | 12 | class ExampleInterface(ServiceInterface): 13 | def __init__(self): 14 | super().__init__('test.interface') 15 | 16 | @method() 17 | def Ping(self): 18 | pass 19 | 20 | @method() 21 | def EchoInt64(self, what: 'x') -> 'x': 22 | return what 23 | 24 | @method() 25 | def EchoString(self, what: 's') -> 's': 26 | return what 27 | 28 | @method() 29 | def ConcatStrings(self, what1: 's', what2: 's') -> 's': 30 | return what1 + what2 31 | 32 | @method() 33 | def EchoThree(self, what1: 's', what2: 's', what3: 's') -> 'sss': 34 | return [what1, what2, what3] 35 | 36 | @method() 37 | def ThrowsError(self): 38 | raise DBusError('test.error', 'something went wrong') 39 | 40 | 41 | @pytest.mark.asyncio 42 | async def test_aio_proxy_object(): 43 | bus_name = 'aio.client.test.methods' 44 | 45 | bus = await aio.MessageBus().connect() 46 | bus2 = await aio.MessageBus().connect() 47 | await bus.request_name(bus_name) 48 | service_interface = ExampleInterface() 49 | bus.export('/test/path', service_interface) 50 | # add some more to test nodes 51 | bus.export('/test/path/child1', ExampleInterface()) 52 | bus.export('/test/path/child2', ExampleInterface()) 53 | 54 | introspection = await bus2.introspect(bus_name, '/test/path') 55 | assert type(introspection) is intr.Node 56 | obj = bus2.get_proxy_object(bus_name, '/test/path', introspection) 57 | interface = obj.get_interface(service_interface.name) 58 | 59 | children = obj.get_children() 60 | assert len(children) == 2 61 | for child in obj.get_children(): 62 | assert type(child) is aio.ProxyObject 63 | 64 | result = await interface.call_ping() 65 | assert result is None 66 | 67 | result = await interface.call_echo_string('hello') 68 | assert result == 'hello' 69 | 70 | result = await interface.call_concat_strings('hello ', 'world') 71 | assert result == 'hello world' 72 | 73 | result = await interface.call_echo_three('hello', 'there', 'world') 74 | assert result == ['hello', 'there', 'world'] 75 | 76 | result = await interface.call_echo_int64(-10000) 77 | assert result == -10000 78 | 79 | result = await interface.call_echo_string('no reply', flags=MessageFlag.NO_REPLY_EXPECTED) 80 | assert result is None 81 | 82 | with pytest.raises(DBusError): 83 | try: 84 | await interface.call_throws_error() 85 | except DBusError as e: 86 | assert e.reply is not None 87 | assert e.type == 'test.error' 88 | assert e.text == 'something went wrong' 89 | raise e 90 | 91 | bus.disconnect() 92 | bus2.disconnect() 93 | 94 | 95 | @pytest.mark.skipif(not has_gi, reason=skip_reason_no_gi) 96 | def test_glib_proxy_object(): 97 | bus_name = 'glib.client.test.methods' 98 | bus = glib.MessageBus().connect_sync() 99 | bus.request_name_sync(bus_name) 100 | service_interface = ExampleInterface() 101 | bus.export('/test/path', service_interface) 102 | 103 | bus2 = glib.MessageBus().connect_sync() 104 | introspection = bus2.introspect_sync(bus_name, '/test/path') 105 | assert type(introspection) is intr.Node 106 | obj = bus.get_proxy_object(bus_name, '/test/path', introspection) 107 | interface = obj.get_interface(service_interface.name) 108 | 109 | result = interface.call_ping_sync() 110 | assert result is None 111 | 112 | result = interface.call_echo_string_sync('hello') 113 | assert result == 'hello' 114 | 115 | result = interface.call_concat_strings_sync('hello ', 'world') 116 | assert result == 'hello world' 117 | 118 | result = interface.call_echo_three_sync('hello', 'there', 'world') 119 | assert result == ['hello', 'there', 'world'] 120 | 121 | with pytest.raises(DBusError): 122 | try: 123 | result = interface.call_throws_error_sync() 124 | assert False, result 125 | except DBusError as e: 126 | assert e.reply is not None 127 | assert e.type == 'test.error' 128 | assert e.text == 'something went wrong' 129 | raise e 130 | 131 | bus.disconnect() 132 | bus2.disconnect() 133 | -------------------------------------------------------------------------------- /test/client/test_properties.py: -------------------------------------------------------------------------------- 1 | from dbus_next import aio, glib, Message, DBusError 2 | from dbus_next.service import ServiceInterface, dbus_property, PropertyAccess 3 | from test.util import check_gi_repository, skip_reason_no_gi 4 | 5 | import pytest 6 | 7 | has_gi = check_gi_repository() 8 | 9 | 10 | class ExampleInterface(ServiceInterface): 11 | def __init__(self): 12 | super().__init__('test.interface') 13 | self._some_property = 'foo' 14 | self.error_name = 'test.error' 15 | self.error_text = 'i am bad' 16 | self._int64_property = -10000 17 | 18 | @dbus_property() 19 | def SomeProperty(self) -> 's': 20 | return self._some_property 21 | 22 | @SomeProperty.setter 23 | def SomeProperty(self, val: 's'): 24 | self._some_property = val 25 | 26 | @dbus_property(access=PropertyAccess.READ) 27 | def Int64Property(self) -> 'x': 28 | return self._int64_property 29 | 30 | @dbus_property() 31 | def ErrorThrowingProperty(self) -> 's': 32 | raise DBusError(self.error_name, self.error_text) 33 | 34 | @ErrorThrowingProperty.setter 35 | def ErrorThrowingProperty(self, val: 's'): 36 | raise DBusError(self.error_name, self.error_text) 37 | 38 | 39 | @pytest.mark.asyncio 40 | async def test_aio_properties(): 41 | service_bus = await aio.MessageBus().connect() 42 | service_interface = ExampleInterface() 43 | service_bus.export('/test/path', service_interface) 44 | 45 | bus = await aio.MessageBus().connect() 46 | obj = bus.get_proxy_object(service_bus.unique_name, '/test/path', 47 | service_bus._introspect_export_path('/test/path')) 48 | interface = obj.get_interface(service_interface.name) 49 | 50 | prop = await interface.get_some_property() 51 | assert prop == service_interface._some_property 52 | 53 | prop = await interface.get_int64_property() 54 | assert prop == service_interface._int64_property 55 | 56 | await interface.set_some_property('different') 57 | assert service_interface._some_property == 'different' 58 | 59 | with pytest.raises(DBusError): 60 | try: 61 | prop = await interface.get_error_throwing_property() 62 | assert False, prop 63 | except DBusError as e: 64 | assert e.type == service_interface.error_name 65 | assert e.text == service_interface.error_text 66 | assert type(e.reply) is Message 67 | raise e 68 | 69 | with pytest.raises(DBusError): 70 | try: 71 | await interface.set_error_throwing_property('different') 72 | except DBusError as e: 73 | assert e.type == service_interface.error_name 74 | assert e.text == service_interface.error_text 75 | assert type(e.reply) is Message 76 | raise e 77 | 78 | service_bus.disconnect() 79 | bus.disconnect() 80 | 81 | 82 | @pytest.mark.skipif(not has_gi, reason=skip_reason_no_gi) 83 | def test_glib_properties(): 84 | service_bus = glib.MessageBus().connect_sync() 85 | service_interface = ExampleInterface() 86 | service_bus.export('/test/path', service_interface) 87 | 88 | bus = glib.MessageBus().connect_sync() 89 | obj = bus.get_proxy_object(service_bus.unique_name, '/test/path', 90 | service_bus._introspect_export_path('/test/path')) 91 | interface = obj.get_interface(service_interface.name) 92 | 93 | prop = interface.get_some_property_sync() 94 | assert prop == service_interface._some_property 95 | 96 | interface.set_some_property_sync('different') 97 | assert service_interface._some_property == 'different' 98 | 99 | with pytest.raises(DBusError): 100 | try: 101 | prop = interface.get_error_throwing_property_sync() 102 | assert False, prop 103 | except DBusError as e: 104 | assert e.type == service_interface.error_name 105 | assert e.text == service_interface.error_text 106 | assert type(e.reply) is Message 107 | raise e 108 | 109 | with pytest.raises(DBusError): 110 | try: 111 | interface.set_error_throwing_property_sync('different2') 112 | except DBusError as e: 113 | assert e.type == service_interface.error_name 114 | assert e.text == service_interface.error_text 115 | assert type(e.reply) is Message 116 | raise e 117 | 118 | service_bus.disconnect() 119 | -------------------------------------------------------------------------------- /test/client/test_signals.py: -------------------------------------------------------------------------------- 1 | from dbus_next.service import ServiceInterface, signal 2 | from dbus_next.aio import MessageBus 3 | from dbus_next import Message 4 | from dbus_next.introspection import Node 5 | from dbus_next.constants import RequestNameReply 6 | 7 | import pytest 8 | 9 | 10 | class ExampleInterface(ServiceInterface): 11 | def __init__(self): 12 | super().__init__('test.interface') 13 | 14 | @signal() 15 | def SomeSignal(self) -> 's': 16 | return 'hello' 17 | 18 | @signal() 19 | def SignalMultiple(self) -> 'ss': 20 | return ['hello', 'world'] 21 | 22 | 23 | @pytest.mark.asyncio 24 | async def test_signals(): 25 | bus1 = await MessageBus().connect() 26 | bus2 = await MessageBus().connect() 27 | 28 | bus_intr = await bus1.introspect('org.freedesktop.DBus', '/org/freedesktop/DBus') 29 | bus_obj = bus1.get_proxy_object('org.freedesktop.DBus', '/org/freedesktop/DBus', bus_intr) 30 | stats = bus_obj.get_interface('org.freedesktop.DBus.Debug.Stats') 31 | 32 | await bus1.request_name('test.signals.name') 33 | service_interface = ExampleInterface() 34 | bus1.export('/test/path', service_interface) 35 | 36 | obj = bus2.get_proxy_object('test.signals.name', '/test/path', 37 | bus1._introspect_export_path('/test/path')) 38 | interface = obj.get_interface(service_interface.name) 39 | 40 | async def ping(): 41 | await bus2.call( 42 | Message(destination=bus1.unique_name, 43 | interface='org.freedesktop.DBus.Peer', 44 | path='/test/path', 45 | member='Ping')) 46 | 47 | err = None 48 | 49 | single_counter = 0 50 | 51 | def single_handler(value): 52 | try: 53 | nonlocal single_counter 54 | nonlocal err 55 | assert value == 'hello' 56 | single_counter += 1 57 | except Exception as e: 58 | err = e 59 | 60 | multiple_counter = 0 61 | 62 | def multiple_handler(value1, value2): 63 | nonlocal multiple_counter 64 | nonlocal err 65 | try: 66 | assert value1 == 'hello' 67 | assert value2 == 'world' 68 | multiple_counter += 1 69 | except Exception as e: 70 | err = e 71 | 72 | await ping() 73 | match_rules = await stats.call_get_all_match_rules() 74 | assert bus2.unique_name in match_rules 75 | bus_match_rules = match_rules[bus2.unique_name] 76 | # the bus connection itself takes a rule on NameOwnerChange after the high 77 | # level client is initialized 78 | assert len(bus_match_rules) == 1 79 | assert len(bus2._user_message_handlers) == 0 80 | 81 | interface.on_some_signal(single_handler) 82 | interface.on_signal_multiple(multiple_handler) 83 | 84 | # Interlude: adding a signal handler with `on_[signal]` should add a match rule and 85 | # message handler. Removing a signal handler with `off_[signal]` should 86 | # remove the match rule and message handler to avoid memory leaks. 87 | await ping() 88 | match_rules = await stats.call_get_all_match_rules() 89 | assert bus2.unique_name in match_rules 90 | bus_match_rules = match_rules[bus2.unique_name] 91 | # test the match rule and user handler has been added 92 | assert len(bus_match_rules) == 2 93 | assert "type='signal',interface='test.interface',path='/test/path',sender='test.signals.name'" in bus_match_rules 94 | assert len(bus2._user_message_handlers) == 1 95 | 96 | service_interface.SomeSignal() 97 | await ping() 98 | assert err is None 99 | assert single_counter == 1 100 | 101 | service_interface.SignalMultiple() 102 | await ping() 103 | assert err is None 104 | assert multiple_counter == 1 105 | 106 | # special case: another bus with the same path and interface but on a 107 | # different name and connection will trigger the match rule of the first 108 | # (happens with mpris) 109 | bus3 = await MessageBus().connect() 110 | await bus3.request_name('test.signals.name2') 111 | service_interface2 = ExampleInterface() 112 | bus3.export('/test/path', service_interface2) 113 | 114 | obj = bus2.get_proxy_object('test.signals.name2', '/test/path', 115 | bus3._introspect_export_path('/test/path')) 116 | # we have to add a dummy handler to add the match rule 117 | iface2 = obj.get_interface(service_interface2.name) 118 | 119 | def dummy_signal_handler(what): 120 | pass 121 | 122 | iface2.on_some_signal(dummy_signal_handler) 123 | await ping() 124 | 125 | service_interface2.SomeSignal() 126 | await ping() 127 | # single_counter is not incremented for signals of the second interface 128 | assert single_counter == 1 129 | 130 | interface.off_some_signal(single_handler) 131 | interface.off_signal_multiple(multiple_handler) 132 | iface2.off_some_signal(dummy_signal_handler) 133 | 134 | # After `off_[signal]`, the match rule and user handler should be removed 135 | await ping() 136 | match_rules = await stats.call_get_all_match_rules() 137 | assert bus2.unique_name in match_rules 138 | bus_match_rules = match_rules[bus2.unique_name] 139 | assert len(bus_match_rules) == 1 140 | assert "type='signal',interface='test.interface',path='/test/path',sender='test.signals.name'" not in bus_match_rules 141 | assert len(bus2._user_message_handlers) == 0 142 | 143 | bus1.disconnect() 144 | bus2.disconnect() 145 | bus3.disconnect() 146 | 147 | 148 | @pytest.mark.asyncio 149 | async def test_signals_with_changing_owners(): 150 | well_known_name = 'test.signals.changing.name' 151 | 152 | bus1 = await MessageBus().connect() 153 | bus2 = await MessageBus().connect() 154 | bus3 = await MessageBus().connect() 155 | 156 | async def ping(): 157 | await bus1.call( 158 | Message(destination=bus1.unique_name, 159 | interface='org.freedesktop.DBus.Peer', 160 | path='/test/path', 161 | member='Ping')) 162 | 163 | service_interface = ExampleInterface() 164 | introspection = Node.default() 165 | introspection.interfaces.append(service_interface.introspect()) 166 | 167 | # get the interface before export 168 | obj = bus1.get_proxy_object(well_known_name, '/test/path', introspection) 169 | iface = obj.get_interface('test.interface') 170 | counter = 0 171 | 172 | def handler(what): 173 | nonlocal counter 174 | counter += 1 175 | 176 | iface.on_some_signal(handler) 177 | await ping() 178 | 179 | # now export and get the name 180 | bus2.export('/test/path', service_interface) 181 | result = await bus2.request_name(well_known_name) 182 | assert result is RequestNameReply.PRIMARY_OWNER 183 | 184 | # the signal should work 185 | service_interface.SomeSignal() 186 | await ping() 187 | assert counter == 1 188 | counter = 0 189 | 190 | # now queue up a transfer of the name 191 | service_interface2 = ExampleInterface() 192 | bus3.export('/test/path', service_interface2) 193 | result = await bus3.request_name(well_known_name) 194 | assert result is RequestNameReply.IN_QUEUE 195 | 196 | # if it doesn't own the name, the signal shouldn't work here 197 | service_interface2.SomeSignal() 198 | await ping() 199 | assert counter == 0 200 | 201 | # now transfer over the name and it should work 202 | bus2.disconnect() 203 | await ping() 204 | 205 | service_interface2.SomeSignal() 206 | await ping() 207 | assert counter == 1 208 | counter = 0 209 | 210 | bus1.disconnect() 211 | bus2.disconnect() 212 | bus3.disconnect() 213 | -------------------------------------------------------------------------------- /test/data/introspection.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /test/service/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/altdesktop/python-dbus-next/ab566e16a71bfc9d7e0d29676aa459ec060e72c5/test/service/__init__.py -------------------------------------------------------------------------------- /test/service/test_decorators.py: -------------------------------------------------------------------------------- 1 | from dbus_next import PropertyAccess, introspection as intr 2 | from dbus_next.service import method, signal, dbus_property, ServiceInterface 3 | 4 | 5 | class ExampleInterface(ServiceInterface): 6 | def __init__(self): 7 | super().__init__('test.interface') 8 | self._some_prop = 55 9 | self._another_prop = 101 10 | self._weird_prop = 500 11 | 12 | @method() 13 | def some_method(self, one: 's', two: 's') -> 's': 14 | return 'hello' 15 | 16 | @method(name='renamed_method', disabled=True) 17 | def another_method(self, eight: 'o', six: 't'): 18 | pass 19 | 20 | @signal() 21 | def some_signal(self) -> 'as': 22 | return ['result'] 23 | 24 | @signal(name='renamed_signal', disabled=True) 25 | def another_signal(self) -> '(dodo)': 26 | return [1, '/', 1, '/'] 27 | 28 | @dbus_property(name='renamed_readonly_property', access=PropertyAccess.READ, disabled=True) 29 | def another_prop(self) -> 't': 30 | return self._another_prop 31 | 32 | @dbus_property() 33 | def some_prop(self) -> 'u': 34 | return self._some_prop 35 | 36 | @some_prop.setter 37 | def some_prop(self, val: 'u'): 38 | self._some_prop = val + 1 39 | 40 | # for this one, the setter has a different name than the getter which is a 41 | # special case in the code 42 | @dbus_property() 43 | def weird_prop(self) -> 't': 44 | return self._weird_prop 45 | 46 | @weird_prop.setter 47 | def setter_for_weird_prop(self, val: 't'): 48 | self._weird_prop = val 49 | 50 | 51 | def test_method_decorator(): 52 | interface = ExampleInterface() 53 | assert interface.name == 'test.interface' 54 | 55 | properties = ServiceInterface._get_properties(interface) 56 | methods = ServiceInterface._get_methods(interface) 57 | signals = ServiceInterface._get_signals(interface) 58 | 59 | assert len(methods) == 2 60 | 61 | method = methods[0] 62 | assert method.name == 'renamed_method' 63 | assert method.in_signature == 'ot' 64 | assert method.out_signature == '' 65 | assert method.disabled 66 | assert type(method.introspection) is intr.Method 67 | 68 | method = methods[1] 69 | assert method.name == 'some_method' 70 | assert method.in_signature == 'ss' 71 | assert method.out_signature == 's' 72 | assert not method.disabled 73 | assert type(method.introspection) is intr.Method 74 | 75 | assert len(signals) == 2 76 | 77 | signal = signals[0] 78 | assert signal.name == 'renamed_signal' 79 | assert signal.signature == '(dodo)' 80 | assert signal.disabled 81 | assert type(signal.introspection) is intr.Signal 82 | 83 | signal = signals[1] 84 | assert signal.name == 'some_signal' 85 | assert signal.signature == 'as' 86 | assert not signal.disabled 87 | assert type(signal.introspection) is intr.Signal 88 | 89 | assert len(properties) == 3 90 | 91 | renamed_readonly_prop = properties[0] 92 | assert renamed_readonly_prop.name == 'renamed_readonly_property' 93 | assert renamed_readonly_prop.signature == 't' 94 | assert renamed_readonly_prop.access == PropertyAccess.READ 95 | assert renamed_readonly_prop.disabled 96 | assert type(renamed_readonly_prop.introspection) is intr.Property 97 | 98 | weird_prop = properties[1] 99 | assert weird_prop.name == 'weird_prop' 100 | assert weird_prop.access == PropertyAccess.READWRITE 101 | assert weird_prop.signature == 't' 102 | assert not weird_prop.disabled 103 | assert weird_prop.prop_getter is not None 104 | assert weird_prop.prop_getter.__name__ == 'weird_prop' 105 | assert weird_prop.prop_setter is not None 106 | assert weird_prop.prop_setter.__name__ == 'setter_for_weird_prop' 107 | assert type(weird_prop.introspection) is intr.Property 108 | 109 | prop = properties[2] 110 | assert prop.name == 'some_prop' 111 | assert prop.access == PropertyAccess.READWRITE 112 | assert prop.signature == 'u' 113 | assert not prop.disabled 114 | assert prop.prop_getter is not None 115 | assert prop.prop_setter is not None 116 | assert type(prop.introspection) is intr.Property 117 | 118 | # make sure the getter and setter actually work 119 | assert interface._some_prop == 55 120 | interface._some_prop = 555 121 | assert interface.some_prop == 555 122 | 123 | assert interface._weird_prop == 500 124 | assert weird_prop.prop_getter(interface) == 500 125 | interface._weird_prop = 1001 126 | assert interface._weird_prop == 1001 127 | weird_prop.prop_setter(interface, 600) 128 | assert interface._weird_prop == 600 129 | 130 | 131 | def test_interface_introspection(): 132 | interface = ExampleInterface() 133 | intr_interface = interface.introspect() 134 | assert type(intr_interface) is intr.Interface 135 | 136 | xml = intr_interface.to_xml() 137 | 138 | assert xml.tag == 'interface' 139 | assert xml.attrib.get('name', None) == 'test.interface' 140 | 141 | methods = xml.findall('method') 142 | signals = xml.findall('signal') 143 | properties = xml.findall('property') 144 | 145 | assert len(xml) == 4 146 | assert len(methods) == 1 147 | assert len(signals) == 1 148 | assert len(properties) == 2 149 | -------------------------------------------------------------------------------- /test/service/test_export.py: -------------------------------------------------------------------------------- 1 | from dbus_next.service import ServiceInterface, method 2 | from dbus_next.aio import MessageBus 3 | from dbus_next import Message, MessageType, introspection as intr 4 | 5 | import pytest 6 | 7 | standard_interfaces_count = len(intr.Node.default().interfaces) 8 | 9 | 10 | class ExampleInterface(ServiceInterface): 11 | def __init__(self, name): 12 | self._method_called = False 13 | super().__init__(name) 14 | 15 | @method() 16 | def some_method(self): 17 | self._method_called = True 18 | 19 | 20 | @pytest.mark.asyncio 21 | async def test_export_unexport(): 22 | interface = ExampleInterface('test.interface') 23 | interface2 = ExampleInterface('test.interface2') 24 | 25 | export_path = '/test/path' 26 | export_path2 = '/test/path/child' 27 | 28 | bus = await MessageBus().connect() 29 | bus.export(export_path, interface) 30 | assert export_path in bus._path_exports 31 | assert len(bus._path_exports[export_path]) == 1 32 | assert bus._path_exports[export_path][0] is interface 33 | assert len(ServiceInterface._get_buses(interface)) == 1 34 | 35 | bus.export(export_path2, interface2) 36 | 37 | node = bus._introspect_export_path(export_path) 38 | assert len(node.interfaces) == standard_interfaces_count + 1 39 | assert len(node.nodes) == 1 40 | # relative path 41 | assert node.nodes[0].name == 'child' 42 | 43 | bus.unexport(export_path, interface) 44 | assert export_path not in bus._path_exports 45 | assert len(ServiceInterface._get_buses(interface)) == 0 46 | 47 | bus.export(export_path2, interface) 48 | assert len(bus._path_exports[export_path2]) == 2 49 | 50 | # test unexporting the whole path 51 | bus.unexport(export_path2) 52 | assert not bus._path_exports 53 | assert not ServiceInterface._get_buses(interface) 54 | assert not ServiceInterface._get_buses(interface2) 55 | 56 | # test unexporting by name 57 | bus.export(export_path, interface) 58 | bus.unexport(export_path, interface.name) 59 | assert not bus._path_exports 60 | assert not ServiceInterface._get_buses(interface) 61 | 62 | node = bus._introspect_export_path('/path/doesnt/exist') 63 | assert type(node) is intr.Node 64 | assert not node.interfaces 65 | assert not node.nodes 66 | 67 | 68 | @pytest.mark.asyncio 69 | async def test_export_alias(): 70 | bus = await MessageBus().connect() 71 | 72 | interface = ExampleInterface('test.interface') 73 | 74 | export_path = '/test/path' 75 | export_path2 = '/test/path/child' 76 | 77 | bus.export(export_path, interface) 78 | bus.export(export_path2, interface) 79 | 80 | result = await bus.call( 81 | Message(destination=bus.unique_name, 82 | path=export_path, 83 | interface='test.interface', 84 | member='some_method')) 85 | assert result.message_type is MessageType.METHOD_RETURN, result.body[0] 86 | 87 | assert interface._method_called 88 | interface._method_called = False 89 | 90 | result = await bus.call( 91 | Message(destination=bus.unique_name, 92 | path=export_path2, 93 | interface='test.interface', 94 | member='some_method')) 95 | assert result.message_type is MessageType.METHOD_RETURN, result.body[0] 96 | assert interface._method_called 97 | 98 | 99 | @pytest.mark.asyncio 100 | async def test_export_introspection(): 101 | interface = ExampleInterface('test.interface') 102 | interface2 = ExampleInterface('test.interface2') 103 | 104 | export_path = '/test/path' 105 | export_path2 = '/test/path/child' 106 | 107 | bus = await MessageBus().connect() 108 | bus.export(export_path, interface) 109 | bus.export(export_path2, interface2) 110 | 111 | root = bus._introspect_export_path('/') 112 | assert len(root.nodes) == 1 113 | -------------------------------------------------------------------------------- /test/service/test_methods.py: -------------------------------------------------------------------------------- 1 | from dbus_next.service import ServiceInterface, method 2 | from dbus_next.aio import MessageBus 3 | from dbus_next import Message, MessageType, ErrorType, Variant, SignatureTree, DBusError, MessageFlag 4 | 5 | import pytest 6 | 7 | 8 | class ExampleInterface(ServiceInterface): 9 | def __init__(self, name): 10 | super().__init__(name) 11 | 12 | @method() 13 | def echo(self, what: 's') -> 's': 14 | assert type(self) is ExampleInterface 15 | return what 16 | 17 | @method() 18 | def echo_multiple(self, what1: 's', what2: 's') -> 'ss': 19 | assert type(self) is ExampleInterface 20 | return [what1, what2] 21 | 22 | @method() 23 | def echo_containers(self, array: 'as', variant: 'v', dict_entries: 'a{sv}', 24 | struct: '(s(s(v)))') -> 'asva{sv}(s(s(v)))': 25 | assert type(self) is ExampleInterface 26 | return [array, variant, dict_entries, struct] 27 | 28 | @method() 29 | def ping(self): 30 | assert type(self) is ExampleInterface 31 | pass 32 | 33 | @method(name='renamed') 34 | def original_name(self): 35 | assert type(self) is ExampleInterface 36 | pass 37 | 38 | @method(disabled=True) 39 | def not_here(self): 40 | assert type(self) is ExampleInterface 41 | pass 42 | 43 | @method() 44 | def throws_unexpected_error(self): 45 | assert type(self) is ExampleInterface 46 | raise Exception('oops') 47 | 48 | @method() 49 | def throws_dbus_error(self): 50 | assert type(self) is ExampleInterface 51 | raise DBusError('test.error', 'an error ocurred') 52 | 53 | 54 | class AsyncInterface(ServiceInterface): 55 | def __init__(self, name): 56 | super().__init__(name) 57 | 58 | @method() 59 | async def echo(self, what: 's') -> 's': 60 | assert type(self) is AsyncInterface 61 | return what 62 | 63 | @method() 64 | async def echo_multiple(self, what1: 's', what2: 's') -> 'ss': 65 | assert type(self) is AsyncInterface 66 | return [what1, what2] 67 | 68 | @method() 69 | async def echo_containers(self, array: 'as', variant: 'v', dict_entries: 'a{sv}', 70 | struct: '(s(s(v)))') -> 'asva{sv}(s(s(v)))': 71 | assert type(self) is AsyncInterface 72 | return [array, variant, dict_entries, struct] 73 | 74 | @method() 75 | async def ping(self): 76 | assert type(self) is AsyncInterface 77 | pass 78 | 79 | @method(name='renamed') 80 | async def original_name(self): 81 | assert type(self) is AsyncInterface 82 | pass 83 | 84 | @method(disabled=True) 85 | async def not_here(self): 86 | assert type(self) is AsyncInterface 87 | pass 88 | 89 | @method() 90 | async def throws_unexpected_error(self): 91 | assert type(self) is AsyncInterface 92 | raise Exception('oops') 93 | 94 | @method() 95 | def throws_dbus_error(self): 96 | assert type(self) is AsyncInterface 97 | raise DBusError('test.error', 'an error ocurred') 98 | 99 | 100 | @pytest.mark.parametrize('interface_class', [ExampleInterface, AsyncInterface]) 101 | @pytest.mark.asyncio 102 | async def test_methods(interface_class): 103 | bus1 = await MessageBus().connect() 104 | bus2 = await MessageBus().connect() 105 | 106 | interface = interface_class('test.interface') 107 | export_path = '/test/path' 108 | 109 | async def call(member, signature='', body=[], flags=MessageFlag.NONE): 110 | return await bus2.call( 111 | Message(destination=bus1.unique_name, 112 | path=export_path, 113 | interface=interface.name, 114 | member=member, 115 | signature=signature, 116 | body=body, 117 | flags=flags)) 118 | 119 | bus1.export(export_path, interface) 120 | 121 | body = ['hello world'] 122 | reply = await call('echo', 's', body) 123 | 124 | assert reply.message_type == MessageType.METHOD_RETURN, reply.body[0] 125 | assert reply.signature == 's' 126 | assert reply.body == body 127 | 128 | body = ['hello', 'world'] 129 | reply = await call('echo_multiple', 'ss', body) 130 | assert reply.message_type == MessageType.METHOD_RETURN, reply.body[0] 131 | assert reply.signature == 'ss' 132 | assert reply.body == body 133 | 134 | body = [['hello', 'world'], 135 | Variant('v', Variant('(ss)', ['hello', 'world'])), { 136 | 'foo': Variant('t', 100) 137 | }, ['one', ['two', [Variant('s', 'three')]]]] 138 | signature = 'asva{sv}(s(s(v)))' 139 | SignatureTree(signature).verify(body) 140 | reply = await call('echo_containers', signature, body) 141 | assert reply.message_type == MessageType.METHOD_RETURN, reply.body[0] 142 | assert reply.signature == signature 143 | assert reply.body == body 144 | 145 | reply = await call('ping') 146 | assert reply.message_type == MessageType.METHOD_RETURN, reply.body[0] 147 | assert reply.signature == '' 148 | assert reply.body == [] 149 | 150 | reply = await call('throws_unexpected_error') 151 | assert reply.message_type == MessageType.ERROR, reply.body[0] 152 | assert reply.error_name == ErrorType.SERVICE_ERROR.value, reply.body[0] 153 | 154 | reply = await call('throws_dbus_error') 155 | assert reply.message_type == MessageType.ERROR, reply.body[0] 156 | assert reply.error_name == 'test.error', reply.body[0] 157 | assert reply.body == ['an error ocurred'] 158 | 159 | reply = await call('ping', flags=MessageFlag.NO_REPLY_EXPECTED) 160 | assert reply is None 161 | 162 | reply = await call('throws_unexpected_error', flags=MessageFlag.NO_REPLY_EXPECTED) 163 | assert reply is None 164 | 165 | reply = await call('throws_dbus_error', flags=MessageFlag.NO_REPLY_EXPECTED) 166 | assert reply is None 167 | -------------------------------------------------------------------------------- /test/service/test_properties.py: -------------------------------------------------------------------------------- 1 | from dbus_next.service import ServiceInterface, dbus_property, method 2 | from dbus_next.aio import MessageBus 3 | from dbus_next import Message, MessageType, PropertyAccess, ErrorType, Variant, DBusError 4 | 5 | import pytest 6 | import asyncio 7 | 8 | 9 | class ExampleInterface(ServiceInterface): 10 | def __init__(self, name): 11 | super().__init__(name) 12 | self._string_prop = 'hi' 13 | self._readonly_prop = 100 14 | self._disabled_prop = '1234' 15 | self._container_prop = [['hello', 'world']] 16 | self._renamed_prop = '65' 17 | 18 | @dbus_property() 19 | def string_prop(self) -> 's': 20 | return self._string_prop 21 | 22 | @string_prop.setter 23 | def string_prop_setter(self, val: 's'): 24 | self._string_prop = val 25 | 26 | @dbus_property(PropertyAccess.READ) 27 | def readonly_prop(self) -> 't': 28 | return self._readonly_prop 29 | 30 | @dbus_property() 31 | def container_prop(self) -> 'a(ss)': 32 | return self._container_prop 33 | 34 | @container_prop.setter 35 | def container_prop(self, val: 'a(ss)'): 36 | self._container_prop = val 37 | 38 | @dbus_property(name='renamed_prop') 39 | def original_name(self) -> 's': 40 | return self._renamed_prop 41 | 42 | @original_name.setter 43 | def original_name_setter(self, val: 's'): 44 | self._renamed_prop = val 45 | 46 | @dbus_property(disabled=True) 47 | def disabled_prop(self) -> 's': 48 | return self._disabled_prop 49 | 50 | @disabled_prop.setter 51 | def disabled_prop(self, val: 's'): 52 | self._disabled_prop = val 53 | 54 | @dbus_property(disabled=True) 55 | def throws_error(self) -> 's': 56 | raise DBusError('test.error', 'told you so') 57 | 58 | @throws_error.setter 59 | def throws_error(self, val: 's'): 60 | raise DBusError('test.error', 'told you so') 61 | 62 | @dbus_property(PropertyAccess.READ, disabled=True) 63 | def returns_wrong_type(self) -> 's': 64 | return 5 65 | 66 | @method() 67 | def do_emit_properties_changed(self): 68 | changed = {'string_prop': 'asdf'} 69 | invalidated = ['container_prop'] 70 | self.emit_properties_changed(changed, invalidated) 71 | 72 | 73 | class AsyncInterface(ServiceInterface): 74 | def __init__(self, name): 75 | super().__init__(name) 76 | self._string_prop = 'hi' 77 | self._readonly_prop = 100 78 | self._disabled_prop = '1234' 79 | self._container_prop = [['hello', 'world']] 80 | self._renamed_prop = '65' 81 | 82 | @dbus_property() 83 | async def string_prop(self) -> 's': 84 | return self._string_prop 85 | 86 | @string_prop.setter 87 | async def string_prop_setter(self, val: 's'): 88 | self._string_prop = val 89 | 90 | @dbus_property(PropertyAccess.READ) 91 | async def readonly_prop(self) -> 't': 92 | return self._readonly_prop 93 | 94 | @dbus_property() 95 | async def container_prop(self) -> 'a(ss)': 96 | return self._container_prop 97 | 98 | @container_prop.setter 99 | async def container_prop(self, val: 'a(ss)'): 100 | self._container_prop = val 101 | 102 | @dbus_property(name='renamed_prop') 103 | async def original_name(self) -> 's': 104 | return self._renamed_prop 105 | 106 | @original_name.setter 107 | async def original_name_setter(self, val: 's'): 108 | self._renamed_prop = val 109 | 110 | @dbus_property(disabled=True) 111 | async def disabled_prop(self) -> 's': 112 | return self._disabled_prop 113 | 114 | @disabled_prop.setter 115 | async def disabled_prop(self, val: 's'): 116 | self._disabled_prop = val 117 | 118 | @dbus_property(disabled=True) 119 | async def throws_error(self) -> 's': 120 | raise DBusError('test.error', 'told you so') 121 | 122 | @throws_error.setter 123 | async def throws_error(self, val: 's'): 124 | raise DBusError('test.error', 'told you so') 125 | 126 | @dbus_property(PropertyAccess.READ, disabled=True) 127 | async def returns_wrong_type(self) -> 's': 128 | return 5 129 | 130 | @method() 131 | def do_emit_properties_changed(self): 132 | changed = {'string_prop': 'asdf'} 133 | invalidated = ['container_prop'] 134 | self.emit_properties_changed(changed, invalidated) 135 | 136 | 137 | @pytest.mark.parametrize('interface_class', [ExampleInterface, AsyncInterface]) 138 | @pytest.mark.asyncio 139 | async def test_property_methods(interface_class): 140 | bus1 = await MessageBus().connect() 141 | bus2 = await MessageBus().connect() 142 | 143 | interface = interface_class('test.interface') 144 | export_path = '/test/path' 145 | bus1.export(export_path, interface) 146 | 147 | async def call_properties(member, signature, body): 148 | return await bus2.call( 149 | Message(destination=bus1.unique_name, 150 | path=export_path, 151 | interface='org.freedesktop.DBus.Properties', 152 | member=member, 153 | signature=signature, 154 | body=body)) 155 | 156 | result = await call_properties('GetAll', 's', [interface.name]) 157 | 158 | assert result.message_type == MessageType.METHOD_RETURN, result.body[0] 159 | assert result.signature == 'a{sv}' 160 | assert result.body == [{ 161 | 'string_prop': Variant('s', interface._string_prop), 162 | 'readonly_prop': Variant('t', interface._readonly_prop), 163 | 'container_prop': Variant('a(ss)', interface._container_prop), 164 | 'renamed_prop': Variant('s', interface._renamed_prop) 165 | }] 166 | 167 | result = await call_properties('Get', 'ss', [interface.name, 'string_prop']) 168 | assert result.message_type == MessageType.METHOD_RETURN, result.body[0] 169 | assert result.signature == 'v' 170 | assert result.body == [Variant('s', 'hi')] 171 | 172 | result = await call_properties( 173 | 'Set', 'ssv', 174 | [interface.name, 'string_prop', Variant('s', 'ho')]) 175 | assert result.message_type == MessageType.METHOD_RETURN, result.body[0] 176 | assert interface._string_prop == 'ho' 177 | if interface_class is AsyncInterface: 178 | assert 'ho', await interface.string_prop() 179 | else: 180 | assert 'ho', interface.string_prop 181 | 182 | result = await call_properties( 183 | 'Set', 'ssv', 184 | [interface.name, 'readonly_prop', Variant('t', 100)]) 185 | assert result.message_type == MessageType.ERROR, result.body[0] 186 | assert result.error_name == ErrorType.PROPERTY_READ_ONLY.value, result.body[0] 187 | 188 | result = await call_properties( 189 | 'Set', 'ssv', 190 | [interface.name, 'disabled_prop', Variant('s', 'asdf')]) 191 | assert result.message_type == MessageType.ERROR, result.body[0] 192 | assert result.error_name == ErrorType.UNKNOWN_PROPERTY.value 193 | 194 | result = await call_properties( 195 | 'Set', 'ssv', 196 | [interface.name, 'not_a_prop', Variant('s', 'asdf')]) 197 | assert result.message_type == MessageType.ERROR, result.body[0] 198 | assert result.error_name == ErrorType.UNKNOWN_PROPERTY.value 199 | 200 | # wrong type 201 | result = await call_properties('Set', 'ssv', [interface.name, 'string_prop', Variant('t', 100)]) 202 | assert result.message_type == MessageType.ERROR 203 | assert result.error_name == ErrorType.INVALID_SIGNATURE.value 204 | 205 | # enable the erroring properties so we can test them 206 | for prop in ServiceInterface._get_properties(interface): 207 | if prop.name in ['throws_error', 'returns_wrong_type']: 208 | prop.disabled = False 209 | 210 | result = await call_properties('Get', 'ss', [interface.name, 'returns_wrong_type']) 211 | assert result.message_type == MessageType.ERROR, result.body[0] 212 | assert result.error_name == ErrorType.SERVICE_ERROR.value 213 | 214 | result = await call_properties( 215 | 'Set', 'ssv', 216 | [interface.name, 'throws_error', Variant('s', 'ho')]) 217 | assert result.message_type == MessageType.ERROR, result.body[0] 218 | assert result.error_name == 'test.error' 219 | assert result.body == ['told you so'] 220 | 221 | result = await call_properties('Get', 'ss', [interface.name, 'throws_error']) 222 | assert result.message_type == MessageType.ERROR, result.body[0] 223 | assert result.error_name == 'test.error' 224 | assert result.body == ['told you so'] 225 | 226 | result = await call_properties('GetAll', 's', [interface.name]) 227 | assert result.message_type == MessageType.ERROR, result.body[0] 228 | assert result.error_name == 'test.error' 229 | assert result.body == ['told you so'] 230 | 231 | 232 | @pytest.mark.parametrize('interface_class', [ExampleInterface, AsyncInterface]) 233 | @pytest.mark.asyncio 234 | async def test_property_changed_signal(interface_class): 235 | bus1 = await MessageBus().connect() 236 | bus2 = await MessageBus().connect() 237 | 238 | await bus2.call( 239 | Message(destination='org.freedesktop.DBus', 240 | path='/org/freedesktop/DBus', 241 | interface='org.freedesktop.DBus', 242 | member='AddMatch', 243 | signature='s', 244 | body=[f'sender={bus1.unique_name}'])) 245 | 246 | interface = interface_class('test.interface') 247 | export_path = '/test/path' 248 | bus1.export(export_path, interface) 249 | 250 | async def wait_for_message(): 251 | # TODO timeout 252 | future = asyncio.get_event_loop().create_future() 253 | 254 | def message_handler(signal): 255 | if signal.interface == 'org.freedesktop.DBus.Properties': 256 | bus2.remove_message_handler(message_handler) 257 | future.set_result(signal) 258 | 259 | bus2.add_message_handler(message_handler) 260 | return await future 261 | 262 | bus2.send( 263 | Message(destination=bus1.unique_name, 264 | interface=interface.name, 265 | path=export_path, 266 | member='do_emit_properties_changed')) 267 | 268 | signal = await wait_for_message() 269 | assert signal.interface == 'org.freedesktop.DBus.Properties' 270 | assert signal.member == 'PropertiesChanged' 271 | assert signal.signature == 'sa{sv}as' 272 | assert signal.body == [ 273 | interface.name, { 274 | 'string_prop': Variant('s', 'asdf') 275 | }, ['container_prop'] 276 | ] 277 | -------------------------------------------------------------------------------- /test/service/test_signals.py: -------------------------------------------------------------------------------- 1 | from dbus_next.service import ServiceInterface, signal, SignalDisabledError, dbus_property 2 | from dbus_next.aio import MessageBus 3 | from dbus_next import Message, MessageType 4 | from dbus_next.constants import PropertyAccess 5 | from dbus_next.signature import Variant 6 | 7 | import pytest 8 | import asyncio 9 | 10 | 11 | class ExampleInterface(ServiceInterface): 12 | def __init__(self, name): 13 | super().__init__(name) 14 | 15 | @signal() 16 | def signal_empty(self): 17 | assert type(self) is ExampleInterface 18 | 19 | @signal() 20 | def signal_simple(self) -> 's': 21 | assert type(self) is ExampleInterface 22 | return 'hello' 23 | 24 | @signal() 25 | def signal_multiple(self) -> 'ss': 26 | assert type(self) is ExampleInterface 27 | return ['hello', 'world'] 28 | 29 | @signal(name='renamed') 30 | def original_name(self): 31 | assert type(self) is ExampleInterface 32 | 33 | @signal(disabled=True) 34 | def signal_disabled(self): 35 | assert type(self) is ExampleInterface 36 | 37 | @dbus_property(access=PropertyAccess.READ) 38 | def test_prop(self) -> 'i': 39 | return 42 40 | 41 | 42 | class SecondExampleInterface(ServiceInterface): 43 | def __init__(self, name): 44 | super().__init__(name) 45 | 46 | @dbus_property(access=PropertyAccess.READ) 47 | def str_prop(self) -> 's': 48 | return "abc" 49 | 50 | @dbus_property(access=PropertyAccess.READ) 51 | def list_prop(self) -> 'ai': 52 | return [1, 2, 3] 53 | 54 | 55 | class ExpectMessage: 56 | def __init__(self, bus1, bus2, interface_name, timeout=1): 57 | self.future = asyncio.get_event_loop().create_future() 58 | self.bus1 = bus1 59 | self.bus2 = bus2 60 | self.interface_name = interface_name 61 | self.timeout = timeout 62 | self.timeout_task = None 63 | 64 | def message_handler(self, msg): 65 | if msg.sender == self.bus1.unique_name and msg.interface == self.interface_name: 66 | self.timeout_task.cancel() 67 | self.future.set_result(msg) 68 | return True 69 | 70 | def timeout_cb(self): 71 | self.future.set_exception(TimeoutError) 72 | 73 | async def __aenter__(self): 74 | self.bus2.add_message_handler(self.message_handler) 75 | self.timeout_task = asyncio.get_event_loop().call_later(self.timeout, self.timeout_cb) 76 | 77 | return self.future 78 | 79 | async def __aexit__(self, exc_type, exc_val, exc_tb): 80 | self.bus2.remove_message_handler(self.message_handler) 81 | 82 | 83 | def assert_signal_ok(signal, export_path, member, signature, body): 84 | assert signal.message_type == MessageType.SIGNAL 85 | assert signal.path == export_path 86 | assert signal.member == member 87 | assert signal.signature == signature 88 | assert signal.body == body 89 | 90 | 91 | @pytest.mark.asyncio 92 | async def test_signals(): 93 | bus1 = await MessageBus().connect() 94 | bus2 = await MessageBus().connect() 95 | 96 | interface = ExampleInterface('test.interface') 97 | export_path = '/test/path' 98 | bus1.export(export_path, interface) 99 | 100 | await bus2.call( 101 | Message(destination='org.freedesktop.DBus', 102 | path='/org/freedesktop/DBus', 103 | interface='org.freedesktop.DBus', 104 | member='AddMatch', 105 | signature='s', 106 | body=[f'sender={bus1.unique_name}'])) 107 | 108 | async with ExpectMessage(bus1, bus2, interface.name) as expected_signal: 109 | interface.signal_empty() 110 | assert_signal_ok(signal=await expected_signal, 111 | export_path=export_path, 112 | member='signal_empty', 113 | signature='', 114 | body=[]) 115 | 116 | async with ExpectMessage(bus1, bus2, interface.name) as expected_signal: 117 | interface.original_name() 118 | assert_signal_ok(signal=await expected_signal, 119 | export_path=export_path, 120 | member='renamed', 121 | signature='', 122 | body=[]) 123 | 124 | async with ExpectMessage(bus1, bus2, interface.name) as expected_signal: 125 | interface.signal_simple() 126 | assert_signal_ok(signal=await expected_signal, 127 | export_path=export_path, 128 | member='signal_simple', 129 | signature='s', 130 | body=['hello']) 131 | 132 | async with ExpectMessage(bus1, bus2, interface.name) as expected_signal: 133 | interface.signal_multiple() 134 | assert_signal_ok(signal=await expected_signal, 135 | export_path=export_path, 136 | member='signal_multiple', 137 | signature='ss', 138 | body=['hello', 'world']) 139 | 140 | with pytest.raises(SignalDisabledError): 141 | interface.signal_disabled() 142 | 143 | 144 | @pytest.mark.asyncio 145 | async def test_interface_add_remove_signal(): 146 | bus1 = await MessageBus().connect() 147 | bus2 = await MessageBus().connect() 148 | 149 | await bus2.call( 150 | Message(destination='org.freedesktop.DBus', 151 | path='/org/freedesktop/DBus', 152 | interface='org.freedesktop.DBus', 153 | member='AddMatch', 154 | signature='s', 155 | body=[f'sender={bus1.unique_name}'])) 156 | 157 | first_interface = ExampleInterface('test.interface.first') 158 | second_interface = SecondExampleInterface('test.interface.second') 159 | export_path = '/test/path' 160 | 161 | # add first interface 162 | async with ExpectMessage(bus1, bus2, 'org.freedesktop.DBus.ObjectManager') as expected_signal: 163 | bus1.export(export_path, first_interface) 164 | assert_signal_ok( 165 | signal=await expected_signal, 166 | export_path=export_path, 167 | member='InterfacesAdded', 168 | signature='oa{sa{sv}}', 169 | body=[export_path, { 170 | 'test.interface.first': { 171 | 'test_prop': Variant('i', 42) 172 | } 173 | }]) 174 | 175 | # add second interface 176 | async with ExpectMessage(bus1, bus2, 'org.freedesktop.DBus.ObjectManager') as expected_signal: 177 | bus1.export(export_path, second_interface) 178 | assert_signal_ok(signal=await expected_signal, 179 | export_path=export_path, 180 | member='InterfacesAdded', 181 | signature='oa{sa{sv}}', 182 | body=[ 183 | export_path, { 184 | 'test.interface.second': { 185 | 'str_prop': Variant('s', "abc"), 186 | 'list_prop': Variant('ai', [1, 2, 3]) 187 | } 188 | } 189 | ]) 190 | 191 | # remove single interface 192 | async with ExpectMessage(bus1, bus2, 'org.freedesktop.DBus.ObjectManager') as expected_signal: 193 | bus1.unexport(export_path, second_interface) 194 | assert_signal_ok(signal=await expected_signal, 195 | export_path=export_path, 196 | member='InterfacesRemoved', 197 | signature='oas', 198 | body=[export_path, ['test.interface.second']]) 199 | 200 | # add second interface again 201 | async with ExpectMessage(bus1, bus2, 'org.freedesktop.DBus.ObjectManager') as expected_signal: 202 | bus1.export(export_path, second_interface) 203 | await expected_signal 204 | 205 | # remove multiple interfaces 206 | async with ExpectMessage(bus1, bus2, 'org.freedesktop.DBus.ObjectManager') as expected_signal: 207 | bus1.unexport(export_path) 208 | assert_signal_ok(signal=await expected_signal, 209 | export_path=export_path, 210 | member='InterfacesRemoved', 211 | signature='oas', 212 | body=[export_path, ['test.interface.first', 'test.interface.second']]) 213 | -------------------------------------------------------------------------------- /test/service/test_standard_interfaces.py: -------------------------------------------------------------------------------- 1 | from dbus_next.service import ServiceInterface, dbus_property, PropertyAccess 2 | from dbus_next.signature import Variant 3 | from dbus_next.aio import MessageBus 4 | from dbus_next import Message, MessageType, introspection as intr 5 | from dbus_next.constants import ErrorType 6 | 7 | import pytest 8 | 9 | standard_interfaces_count = len(intr.Node.default().interfaces) 10 | 11 | 12 | class ExampleInterface(ServiceInterface): 13 | def __init__(self, name): 14 | super().__init__(name) 15 | 16 | 17 | class ExampleComplexInterface(ServiceInterface): 18 | def __init__(self, name): 19 | self._foo = 42 20 | self._bar = 'str' 21 | self._async_prop = 'async' 22 | super().__init__(name) 23 | 24 | @dbus_property(access=PropertyAccess.READ) 25 | def Foo(self) -> 'y': 26 | return self._foo 27 | 28 | @dbus_property(access=PropertyAccess.READ) 29 | def Bar(self) -> 's': 30 | return self._bar 31 | 32 | @dbus_property(access=PropertyAccess.READ) 33 | async def AsyncProp(self) -> 's': 34 | return self._async_prop 35 | 36 | 37 | @pytest.mark.asyncio 38 | async def test_introspectable_interface(): 39 | bus1 = await MessageBus().connect() 40 | bus2 = await MessageBus().connect() 41 | 42 | interface = ExampleInterface('test.interface') 43 | interface2 = ExampleInterface('test.interface2') 44 | 45 | export_path = '/test/path' 46 | bus1.export(export_path, interface) 47 | bus1.export(export_path, interface2) 48 | 49 | reply = await bus2.call( 50 | Message(destination=bus1.unique_name, 51 | path=export_path, 52 | interface='org.freedesktop.DBus.Introspectable', 53 | member='Introspect')) 54 | 55 | assert reply.message_type == MessageType.METHOD_RETURN, reply.body[0] 56 | assert reply.signature == 's' 57 | node = intr.Node.parse(reply.body[0]) 58 | assert len(node.interfaces) == standard_interfaces_count + 2 59 | assert node.interfaces[-1].name == 'test.interface2' 60 | assert node.interfaces[-2].name == 'test.interface' 61 | assert not node.nodes 62 | 63 | # introspect works on every path 64 | reply = await bus2.call( 65 | Message(destination=bus1.unique_name, 66 | path='/path/doesnt/exist', 67 | interface='org.freedesktop.DBus.Introspectable', 68 | member='Introspect')) 69 | assert reply.message_type == MessageType.METHOD_RETURN, reply.body[0] 70 | assert reply.signature == 's' 71 | node = intr.Node.parse(reply.body[0]) 72 | assert not node.interfaces 73 | assert not node.nodes 74 | 75 | 76 | @pytest.mark.asyncio 77 | async def test_peer_interface(): 78 | bus1 = await MessageBus().connect() 79 | bus2 = await MessageBus().connect() 80 | 81 | reply = await bus2.call( 82 | Message(destination=bus1.unique_name, 83 | path='/path/doesnt/exist', 84 | interface='org.freedesktop.DBus.Peer', 85 | member='Ping')) 86 | 87 | assert reply.message_type == MessageType.METHOD_RETURN, reply.body[0] 88 | assert reply.signature == '' 89 | 90 | reply = await bus2.call( 91 | Message(destination=bus1.unique_name, 92 | path='/path/doesnt/exist', 93 | interface='org.freedesktop.DBus.Peer', 94 | member='GetMachineId', 95 | signature='')) 96 | 97 | assert reply.message_type == MessageType.METHOD_RETURN, reply.body[0] 98 | assert reply.signature == 's' 99 | 100 | 101 | @pytest.mark.asyncio 102 | async def test_object_manager(): 103 | expected_reply = { 104 | '/test/path/deeper': { 105 | 'test.interface2': { 106 | 'Bar': Variant('s', 'str'), 107 | 'Foo': Variant('y', 42), 108 | 'AsyncProp': Variant('s', 'async'), 109 | } 110 | } 111 | } 112 | reply_ext = { 113 | '/test/path': { 114 | 'test.interface1': {}, 115 | 'test.interface2': { 116 | 'Bar': Variant('s', 'str'), 117 | 'Foo': Variant('y', 42), 118 | 'AsyncProp': Variant('s', 'async'), 119 | } 120 | } 121 | } 122 | 123 | bus1 = await MessageBus().connect() 124 | bus2 = await MessageBus().connect() 125 | 126 | interface = ExampleInterface('test.interface1') 127 | interface2 = ExampleComplexInterface('test.interface2') 128 | 129 | export_path = '/test/path' 130 | bus1.export(export_path, interface) 131 | bus1.export(export_path, interface2) 132 | bus1.export(export_path + '/deeper', interface2) 133 | 134 | reply_root = await bus2.call( 135 | Message(destination=bus1.unique_name, 136 | path='/', 137 | interface='org.freedesktop.DBus.ObjectManager', 138 | member='GetManagedObjects')) 139 | 140 | reply_level1 = await bus2.call( 141 | Message(destination=bus1.unique_name, 142 | path=export_path, 143 | interface='org.freedesktop.DBus.ObjectManager', 144 | member='GetManagedObjects')) 145 | 146 | reply_level2 = await bus2.call( 147 | Message(destination=bus1.unique_name, 148 | path=export_path + '/deeper', 149 | interface='org.freedesktop.DBus.ObjectManager', 150 | member='GetManagedObjects')) 151 | 152 | assert reply_root.signature == 'a{oa{sa{sv}}}' 153 | assert reply_level1.signature == 'a{oa{sa{sv}}}' 154 | assert reply_level2.signature == 'a{oa{sa{sv}}}' 155 | 156 | assert reply_level2.body == [{}] 157 | assert reply_level1.body == [expected_reply] 158 | expected_reply.update(reply_ext) 159 | assert reply_root.body == [expected_reply] 160 | 161 | 162 | @pytest.mark.asyncio 163 | async def test_standard_interface_properties(): 164 | # standard interfaces have no properties, but should still behave correctly 165 | # when you try to call the methods anyway (#49) 166 | bus1 = await MessageBus().connect() 167 | bus2 = await MessageBus().connect() 168 | interface = ExampleInterface('test.interface1') 169 | export_path = '/test/path' 170 | bus1.export(export_path, interface) 171 | 172 | for iface in [ 173 | 'org.freedesktop.DBus.Properties', 'org.freedesktop.DBus.Introspectable', 174 | 'org.freedesktop.DBus.Peer', 'org.freedesktop.DBus.ObjectManager' 175 | ]: 176 | 177 | result = await bus2.call( 178 | Message(destination=bus1.unique_name, 179 | path=export_path, 180 | interface='org.freedesktop.DBus.Properties', 181 | member='Get', 182 | signature='ss', 183 | body=[iface, 'anything'])) 184 | assert result.message_type is MessageType.ERROR 185 | assert result.error_name == ErrorType.UNKNOWN_PROPERTY.value 186 | 187 | result = await bus2.call( 188 | Message(destination=bus1.unique_name, 189 | path=export_path, 190 | interface='org.freedesktop.DBus.Properties', 191 | member='Set', 192 | signature='ssv', 193 | body=[iface, 'anything', Variant('s', 'new thing')])) 194 | assert result.message_type is MessageType.ERROR 195 | assert result.error_name == ErrorType.UNKNOWN_PROPERTY.value 196 | 197 | result = await bus2.call( 198 | Message(destination=bus1.unique_name, 199 | path=export_path, 200 | interface='org.freedesktop.DBus.Properties', 201 | member='GetAll', 202 | signature='s', 203 | body=[iface])) 204 | assert result.message_type is MessageType.METHOD_RETURN 205 | assert result.body == [{}] 206 | -------------------------------------------------------------------------------- /test/test_address_parser.py: -------------------------------------------------------------------------------- 1 | from dbus_next._private.address import parse_address 2 | 3 | 4 | def test_valid_addresses(): 5 | 6 | valid_addresses = { 7 | 'unix:path=/run/user/1000/bus': [('unix', { 8 | 'path': '/run/user/1000/bus' 9 | })], 10 | 'unix:abstract=/tmp/dbus-ft9sODWpZk,guid=a7b1d5912379c2d471165e9b5cb74a03': [('unix', { 11 | 'abstract': 12 | '/tmp/dbus-ft9sODWpZk', 13 | 'guid': 14 | 'a7b1d5912379c2d471165e9b5cb74a03' 15 | })], 16 | 'unix1:key1=val1;unix2:key2=val2': [('unix1', { 17 | 'key1': 'val1' 18 | }), ('unix2', { 19 | 'key2': 'val2' 20 | })], 21 | 'unix:escaped=hello%20world': [('unix', { 22 | 'escaped': 'hello world' 23 | })], 24 | 'tcp:host=127.0.0.1,port=55556': [('tcp', { 25 | 'host': '127.0.0.1', 26 | 'port': '55556' 27 | })] 28 | } 29 | 30 | for address, parsed in valid_addresses.items(): 31 | assert parse_address(address) == parsed 32 | -------------------------------------------------------------------------------- /test/test_aio_low_level.py: -------------------------------------------------------------------------------- 1 | from dbus_next.aio import MessageBus 2 | from dbus_next import Message, MessageType, MessageFlag 3 | 4 | import pytest 5 | 6 | 7 | @pytest.mark.asyncio 8 | async def test_standard_interfaces(): 9 | bus = await MessageBus().connect() 10 | msg = Message(destination='org.freedesktop.DBus', 11 | path='/org/freedesktop/DBus', 12 | interface='org.freedesktop.DBus', 13 | member='ListNames', 14 | serial=bus.next_serial()) 15 | reply = await bus.call(msg) 16 | 17 | assert reply.message_type == MessageType.METHOD_RETURN 18 | assert reply.reply_serial == msg.serial 19 | assert reply.signature == 'as' 20 | assert bus.unique_name in reply.body[0] 21 | 22 | msg.interface = 'org.freedesktop.DBus.Introspectable' 23 | msg.member = 'Introspect' 24 | msg.serial = bus.next_serial() 25 | 26 | reply = await bus.call(msg) 27 | assert reply.message_type == MessageType.METHOD_RETURN 28 | assert reply.reply_serial == msg.serial 29 | assert reply.signature == 's' 30 | assert type(reply.body[0]) is str 31 | 32 | msg.member = 'MemberDoesNotExist' 33 | msg.serial = bus.next_serial() 34 | 35 | reply = await bus.call(msg) 36 | assert reply.message_type == MessageType.ERROR 37 | assert reply.reply_serial == msg.serial 38 | assert reply.error_name 39 | assert reply.signature == 's' 40 | assert type(reply.body[0]) is str 41 | 42 | 43 | @pytest.mark.asyncio 44 | async def test_sending_messages_between_buses(): 45 | bus1 = await MessageBus().connect() 46 | bus2 = await MessageBus().connect() 47 | 48 | msg = Message(destination=bus1.unique_name, 49 | path='/org/test/path', 50 | interface='org.test.iface', 51 | member='SomeMember', 52 | serial=bus2.next_serial()) 53 | 54 | def message_handler(sent): 55 | if sent.sender == bus2.unique_name and sent.serial == msg.serial: 56 | assert sent.path == msg.path 57 | assert sent.serial == msg.serial 58 | assert sent.interface == msg.interface 59 | assert sent.member == msg.member 60 | bus1.send(Message.new_method_return(sent, 's', ['got it'])) 61 | bus1.remove_message_handler(message_handler) 62 | return True 63 | 64 | bus1.add_message_handler(message_handler) 65 | 66 | reply = await bus2.call(msg) 67 | 68 | assert reply.message_type == MessageType.METHOD_RETURN 69 | assert reply.sender == bus1.unique_name 70 | assert reply.signature == 's' 71 | assert reply.body == ['got it'] 72 | assert reply.reply_serial == msg.serial 73 | 74 | def message_handler_error(sent): 75 | if sent.sender == bus2.unique_name and sent.serial == msg.serial: 76 | assert sent.path == msg.path 77 | assert sent.serial == msg.serial 78 | assert sent.interface == msg.interface 79 | assert sent.member == msg.member 80 | bus1.send(Message.new_error(sent, 'org.test.Error', 'throwing an error')) 81 | bus1.remove_message_handler(message_handler_error) 82 | return True 83 | 84 | bus1.add_message_handler(message_handler_error) 85 | 86 | msg.serial = bus2.next_serial() 87 | 88 | reply = await bus2.call(msg) 89 | 90 | assert reply.message_type == MessageType.ERROR 91 | assert reply.sender == bus1.unique_name 92 | assert reply.reply_serial == msg.serial 93 | assert reply.error_name == 'org.test.Error' 94 | assert reply.signature == 's' 95 | assert reply.body == ['throwing an error'] 96 | 97 | msg.serial = bus2.next_serial() 98 | msg.flags = MessageFlag.NO_REPLY_EXPECTED 99 | reply = await bus2.call(msg) 100 | assert reply is None 101 | 102 | 103 | @pytest.mark.asyncio 104 | async def test_sending_signals_between_buses(event_loop): 105 | bus1 = await MessageBus().connect() 106 | bus2 = await MessageBus().connect() 107 | 108 | add_match_msg = Message(destination='org.freedesktop.DBus', 109 | path='/org/freedesktop/DBus', 110 | interface='org.freedesktop.DBus', 111 | member='AddMatch', 112 | signature='s', 113 | body=[f'sender={bus2.unique_name}']) 114 | 115 | await bus1.call(add_match_msg) 116 | 117 | async def wait_for_message(): 118 | future = event_loop.create_future() 119 | 120 | def message_handler(signal): 121 | if signal.sender == bus2.unique_name: 122 | bus1.remove_message_handler(message_handler) 123 | future.set_result(signal) 124 | 125 | bus1.add_message_handler(message_handler) 126 | return await future 127 | 128 | bus2.send( 129 | Message.new_signal('/org/test/path', 'org.test.interface', 'SomeSignal', 's', ['a signal'])) 130 | 131 | signal = await wait_for_message() 132 | 133 | assert signal.message_type == MessageType.SIGNAL 134 | assert signal.path == '/org/test/path' 135 | assert signal.interface == 'org.test.interface' 136 | assert signal.member == 'SomeSignal' 137 | assert signal.signature == 's' 138 | assert signal.body == ['a signal'] 139 | -------------------------------------------------------------------------------- /test/test_big_message.py: -------------------------------------------------------------------------------- 1 | from dbus_next import aio, glib, Message, MessageType 2 | from dbus_next.service import ServiceInterface, method 3 | from test.util import check_gi_repository, skip_reason_no_gi 4 | 5 | import pytest 6 | 7 | has_gi = check_gi_repository() 8 | 9 | 10 | class ExampleInterface(ServiceInterface): 11 | def __init__(self): 12 | super().__init__('example.interface') 13 | 14 | @method() 15 | def echo_bytes(self, what: 'ay') -> 'ay': 16 | return what 17 | 18 | 19 | @pytest.mark.asyncio 20 | async def test_aio_big_message(): 21 | 'this tests that nonblocking reads and writes actually work for aio' 22 | bus1 = await aio.MessageBus().connect() 23 | bus2 = await aio.MessageBus().connect() 24 | interface = ExampleInterface() 25 | bus1.export('/test/path', interface) 26 | 27 | # two megabytes 28 | big_body = [bytes(1000000) * 2] 29 | result = await bus2.call( 30 | Message(destination=bus1.unique_name, 31 | path='/test/path', 32 | interface=interface.name, 33 | member='echo_bytes', 34 | signature='ay', 35 | body=big_body)) 36 | assert result.message_type == MessageType.METHOD_RETURN, result.body[0] 37 | assert result.body[0] == big_body[0] 38 | 39 | 40 | @pytest.mark.skipif(not has_gi, reason=skip_reason_no_gi) 41 | def test_glib_big_message(): 42 | 'this tests that nonblocking reads and writes actually work for glib' 43 | bus1 = glib.MessageBus().connect_sync() 44 | bus2 = glib.MessageBus().connect_sync() 45 | interface = ExampleInterface() 46 | bus1.export('/test/path', interface) 47 | 48 | # two megabytes 49 | big_body = [bytes(1000000) * 2] 50 | result = bus2.call_sync( 51 | Message(destination=bus1.unique_name, 52 | path='/test/path', 53 | interface=interface.name, 54 | member='echo_bytes', 55 | signature='ay', 56 | body=big_body)) 57 | assert result.message_type == MessageType.METHOD_RETURN, result.body[0] 58 | assert result.body[0] == big_body[0] 59 | -------------------------------------------------------------------------------- /test/test_disconnect.py: -------------------------------------------------------------------------------- 1 | from dbus_next.aio import MessageBus 2 | from dbus_next import Message 3 | 4 | import os 5 | import pytest 6 | import functools 7 | 8 | 9 | @pytest.mark.asyncio 10 | async def test_bus_disconnect_before_reply(event_loop): 11 | '''In this test, the bus disconnects before the reply comes in. Make sure 12 | the caller receives a reply with the error instead of hanging.''' 13 | bus = MessageBus() 14 | assert not bus.connected 15 | await bus.connect() 16 | assert bus.connected 17 | 18 | ping = bus.call( 19 | Message(destination='org.freedesktop.DBus', 20 | path='/org/freedesktop/DBus', 21 | interface='org.freedesktop.DBus', 22 | member='Ping')) 23 | 24 | event_loop.call_soon(bus.disconnect) 25 | 26 | with pytest.raises((EOFError, BrokenPipeError)): 27 | await ping 28 | 29 | assert bus._disconnected 30 | assert not bus.connected 31 | assert (await bus.wait_for_disconnect()) is None 32 | 33 | 34 | @pytest.mark.asyncio 35 | async def test_unexpected_disconnect(event_loop): 36 | bus = MessageBus() 37 | assert not bus.connected 38 | await bus.connect() 39 | assert bus.connected 40 | 41 | ping = bus.call( 42 | Message(destination='org.freedesktop.DBus', 43 | path='/org/freedesktop/DBus', 44 | interface='org.freedesktop.DBus', 45 | member='Ping')) 46 | 47 | event_loop.call_soon(functools.partial(os.close, bus._fd)) 48 | 49 | with pytest.raises(OSError): 50 | await ping 51 | 52 | assert bus._disconnected 53 | assert not bus.connected 54 | 55 | with pytest.raises(OSError): 56 | await bus.wait_for_disconnect() 57 | -------------------------------------------------------------------------------- /test/test_glib_low_level.py: -------------------------------------------------------------------------------- 1 | from dbus_next.glib import MessageBus 2 | from dbus_next import Message, MessageType, MessageFlag 3 | from test.util import check_gi_repository, skip_reason_no_gi 4 | 5 | import pytest 6 | 7 | has_gi = check_gi_repository() 8 | 9 | if has_gi: 10 | from gi.repository import GLib 11 | 12 | 13 | @pytest.mark.skipif(not has_gi, reason=skip_reason_no_gi) 14 | def test_standard_interfaces(): 15 | bus = MessageBus().connect_sync() 16 | msg = Message(destination='org.freedesktop.DBus', 17 | path='/org/freedesktop/DBus', 18 | interface='org.freedesktop.DBus', 19 | member='ListNames', 20 | serial=bus.next_serial()) 21 | reply = bus.call_sync(msg) 22 | 23 | assert reply.message_type == MessageType.METHOD_RETURN 24 | assert reply.reply_serial == msg.serial 25 | assert reply.signature == 'as' 26 | assert bus.unique_name in reply.body[0] 27 | 28 | msg.interface = 'org.freedesktop.DBus.Introspectable' 29 | msg.member = 'Introspect' 30 | msg.serial = bus.next_serial() 31 | 32 | reply = bus.call_sync(msg) 33 | assert reply.message_type == MessageType.METHOD_RETURN 34 | assert reply.reply_serial == msg.serial 35 | assert reply.signature == 's' 36 | assert type(reply.body[0]) is str 37 | 38 | msg.member = 'MemberDoesNotExist' 39 | msg.serial = bus.next_serial() 40 | 41 | reply = bus.call_sync(msg) 42 | assert reply.message_type == MessageType.ERROR 43 | assert reply.reply_serial == msg.serial 44 | assert reply.error_name 45 | assert reply.signature == 's' 46 | assert type(reply.body[0]) is str 47 | 48 | 49 | @pytest.mark.skipif(not has_gi, reason=skip_reason_no_gi) 50 | def test_sending_messages_between_buses(): 51 | bus1 = MessageBus().connect_sync() 52 | bus2 = MessageBus().connect_sync() 53 | 54 | msg = Message(destination=bus1.unique_name, 55 | path='/org/test/path', 56 | interface='org.test.iface', 57 | member='SomeMember', 58 | serial=bus2.next_serial()) 59 | 60 | def message_handler(sent): 61 | if sent.sender == bus2.unique_name and sent.serial == msg.serial: 62 | assert sent.path == msg.path 63 | assert sent.serial == msg.serial 64 | assert sent.interface == msg.interface 65 | assert sent.member == msg.member 66 | bus1.send(Message.new_method_return(sent, 's', ['got it'])) 67 | bus1.remove_message_handler(message_handler) 68 | return True 69 | 70 | bus1.add_message_handler(message_handler) 71 | 72 | reply = bus2.call_sync(msg) 73 | 74 | assert reply.message_type == MessageType.METHOD_RETURN, reply.body[0] 75 | assert reply.sender == bus1.unique_name 76 | assert reply.signature == 's' 77 | assert reply.body == ['got it'] 78 | assert reply.reply_serial == msg.serial 79 | 80 | def message_handler_error(sent): 81 | if sent.sender == bus2.unique_name and sent.serial == msg.serial: 82 | assert sent.path == msg.path 83 | assert sent.serial == msg.serial 84 | assert sent.interface == msg.interface 85 | assert sent.member == msg.member 86 | bus1.send(Message.new_error(sent, 'org.test.Error', 'throwing an error')) 87 | bus1.remove_message_handler(message_handler_error) 88 | return True 89 | 90 | bus1.add_message_handler(message_handler_error) 91 | 92 | msg.serial = bus2.next_serial() 93 | 94 | reply = bus2.call_sync(msg) 95 | 96 | assert reply.message_type == MessageType.ERROR 97 | assert reply.sender == bus1.unique_name 98 | assert reply.reply_serial == msg.serial 99 | assert reply.error_name == 'org.test.Error' 100 | assert reply.signature == 's' 101 | assert reply.body == ['throwing an error'] 102 | 103 | msg.serial = bus2.next_serial() 104 | msg.flags = MessageFlag.NO_REPLY_EXPECTED 105 | reply = bus2.call_sync(msg) 106 | assert reply is None 107 | 108 | 109 | @pytest.mark.skipif(not has_gi, reason=skip_reason_no_gi) 110 | def test_sending_signals_between_buses(): 111 | bus1 = MessageBus().connect_sync() 112 | bus2 = MessageBus().connect_sync() 113 | 114 | add_match_msg = Message(destination='org.freedesktop.DBus', 115 | path='/org/freedesktop/DBus', 116 | interface='org.freedesktop.DBus', 117 | member='AddMatch', 118 | signature='s', 119 | body=[f'sender={bus2.unique_name}']) 120 | 121 | bus1.call_sync(add_match_msg) 122 | 123 | main = GLib.MainLoop() 124 | 125 | def wait_for_message(): 126 | ret = None 127 | 128 | def message_handler(signal): 129 | nonlocal ret 130 | if signal.sender == bus2.unique_name: 131 | ret = signal 132 | bus1.remove_message_handler(message_handler) 133 | main.quit() 134 | 135 | bus1.add_message_handler(message_handler) 136 | main.run() 137 | return ret 138 | 139 | bus2.send( 140 | Message.new_signal('/org/test/path', 'org.test.interface', 'SomeSignal', 's', ['a signal'])) 141 | 142 | signal = wait_for_message() 143 | 144 | assert signal.message_type == MessageType.SIGNAL 145 | assert signal.path == '/org/test/path' 146 | assert signal.interface == 'org.test.interface' 147 | assert signal.member == 'SomeSignal' 148 | assert signal.signature == 's' 149 | assert signal.body == ['a signal'] 150 | -------------------------------------------------------------------------------- /test/test_introspection.py: -------------------------------------------------------------------------------- 1 | from dbus_next import introspection as intr, ArgDirection, PropertyAccess, SignatureType 2 | 3 | import os 4 | 5 | example_data = open(f'{os.path.dirname(__file__)}/data/introspection.xml', 'r').read() 6 | 7 | 8 | def test_example_introspection_from_xml(): 9 | node = intr.Node.parse(example_data) 10 | 11 | assert len(node.interfaces) == 1 12 | interface = node.interfaces[0] 13 | 14 | assert len(node.nodes) == 2 15 | assert len(interface.methods) == 3 16 | assert len(interface.signals) == 2 17 | assert len(interface.properties) == 1 18 | 19 | assert type(node.nodes[0]) is intr.Node 20 | assert node.nodes[0].name == 'child_of_sample_object' 21 | assert type(node.nodes[1]) is intr.Node 22 | assert node.nodes[1].name == 'another_child_of_sample_object' 23 | 24 | assert interface.name == 'com.example.SampleInterface0' 25 | 26 | frobate = interface.methods[0] 27 | assert type(frobate) is intr.Method 28 | assert frobate.name == 'Frobate' 29 | assert len(frobate.in_args) == 1 30 | assert len(frobate.out_args) == 2 31 | 32 | foo = frobate.in_args[0] 33 | assert type(foo) is intr.Arg 34 | assert foo.name == 'foo' 35 | assert foo.direction == ArgDirection.IN 36 | assert foo.signature == 'i' 37 | assert type(foo.type) is SignatureType 38 | assert foo.type.token == 'i' 39 | 40 | bar = frobate.out_args[0] 41 | assert type(bar) is intr.Arg 42 | assert bar.name == 'bar' 43 | assert bar.direction == ArgDirection.OUT 44 | assert bar.signature == 's' 45 | assert type(bar.type) is SignatureType 46 | assert bar.type.token == 's' 47 | 48 | prop = interface.properties[0] 49 | assert type(prop) is intr.Property 50 | assert prop.name == 'Bar' 51 | assert prop.signature == 'y' 52 | assert type(prop.type) is SignatureType 53 | assert prop.type.token == 'y' 54 | assert prop.access == PropertyAccess.WRITE 55 | 56 | changed = interface.signals[0] 57 | assert type(changed) is intr.Signal 58 | assert changed.name == 'Changed' 59 | assert len(changed.args) == 1 60 | new_value = changed.args[0] 61 | assert type(new_value) is intr.Arg 62 | assert new_value.name == 'new_value' 63 | assert new_value.signature == 'b' 64 | 65 | 66 | def test_example_introspection_to_xml(): 67 | node = intr.Node.parse(example_data) 68 | tree = node.to_xml() 69 | assert tree.tag == 'node' 70 | assert tree.attrib.get('name') == '/com/example/sample_object0' 71 | assert len(tree) == 3 72 | interface = tree[0] 73 | assert interface.tag == 'interface' 74 | assert interface.get('name') == 'com.example.SampleInterface0' 75 | assert len(interface) == 6 76 | method = interface[0] 77 | assert method.tag == 'method' 78 | assert method.get('name') == 'Frobate' 79 | # TODO annotations 80 | assert len(method) == 3 81 | 82 | arg = method[0] 83 | assert arg.tag == 'arg' 84 | assert arg.attrib.get('name') == 'foo' 85 | assert arg.attrib.get('type') == 'i' 86 | assert arg.attrib.get('direction') == 'in' 87 | 88 | signal = interface[3] 89 | assert signal.tag == 'signal' 90 | assert signal.attrib.get('name') == 'Changed' 91 | assert len(signal) == 1 92 | 93 | arg = signal[0] 94 | assert arg.tag == 'arg' 95 | assert arg.attrib.get('name') == 'new_value' 96 | assert arg.attrib.get('type') == 'b' 97 | 98 | signal = interface[4] 99 | assert signal.tag == 'signal' 100 | assert signal.attrib.get('name') == 'ChangedMulti' 101 | assert len(signal) == 2 102 | 103 | arg = signal[0] 104 | assert arg.tag == 'arg' 105 | assert arg.attrib.get('name') == 'new_value1' 106 | assert arg.attrib.get('type') == 'b' 107 | 108 | arg = signal[1] 109 | assert arg.tag == 'arg' 110 | assert arg.attrib.get('name') == 'new_value2' 111 | assert arg.attrib.get('type') == 'y' 112 | 113 | prop = interface[5] 114 | assert prop.attrib.get('name') == 'Bar' 115 | assert prop.attrib.get('type') == 'y' 116 | assert prop.attrib.get('access') == 'write' 117 | 118 | 119 | def test_default_interfaces(): 120 | # just make sure it doesn't throw 121 | default = intr.Node.default() 122 | assert type(default) is intr.Node 123 | -------------------------------------------------------------------------------- /test/test_marshaller.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict 2 | from dbus_next._private.unmarshaller import Unmarshaller 3 | from dbus_next import Message, Variant, SignatureTree, MessageType, MessageFlag 4 | 5 | import json 6 | import os 7 | import io 8 | 9 | import pytest 10 | 11 | 12 | def print_buf(buf): 13 | i = 0 14 | while True: 15 | p = buf[i:i + 8] 16 | if not p: 17 | break 18 | print(p) 19 | i += 8 20 | 21 | 22 | # these messages have been verified with another library 23 | table = json.load(open(os.path.dirname(__file__) + "/data/messages.json")) 24 | 25 | 26 | def json_to_message(message: Dict[str, Any]) -> Message: 27 | copy = dict(message) 28 | if "message_type" in copy: 29 | copy["message_type"] = MessageType(copy["message_type"]) 30 | if "flags" in copy: 31 | copy["flags"] = MessageFlag(copy["flags"]) 32 | 33 | return Message(**copy) 34 | 35 | 36 | # variants are an object in the json 37 | def replace_variants(type_, item): 38 | if type_.token == "v" and type(item) is not Variant: 39 | item = Variant( 40 | item["signature"], 41 | replace_variants(SignatureTree(item["signature"]).types[0], item["value"]), 42 | ) 43 | elif type_.token == "a": 44 | for i, item_child in enumerate(item): 45 | if type_.children[0].token == "{": 46 | for k, v in item.items(): 47 | item[k] = replace_variants(type_.children[0].children[1], v) 48 | else: 49 | item[i] = replace_variants(type_.children[0], item_child) 50 | elif type_.token == "(": 51 | for i, item_child in enumerate(item): 52 | if type_.children[0].token == "{": 53 | assert False 54 | else: 55 | item[i] = replace_variants(type_.children[i], item_child) 56 | 57 | return item 58 | 59 | 60 | def json_dump(what): 61 | def dumper(obj): 62 | try: 63 | return obj.toJSON() 64 | except Exception: 65 | return obj.__dict__ 66 | 67 | return json.dumps(what, default=dumper, indent=2) 68 | 69 | 70 | def test_marshalling_with_table(): 71 | for item in table: 72 | message = json_to_message(item["message"]) 73 | 74 | body = [] 75 | for i, type_ in enumerate(message.signature_tree.types): 76 | body.append(replace_variants(type_, message.body[i])) 77 | message.body = body 78 | 79 | buf = message._marshall() 80 | data = bytes.fromhex(item["data"]) 81 | 82 | if buf != data: 83 | print("message:") 84 | print(json_dump(item["message"])) 85 | print("") 86 | print("mine:") 87 | print_buf(bytes(buf)) 88 | print("") 89 | print("theirs:") 90 | print_buf(data) 91 | 92 | assert buf == data 93 | 94 | 95 | @pytest.mark.parametrize("unmarshall_table", (table, )) 96 | def test_unmarshalling_with_table(unmarshall_table): 97 | for item in unmarshall_table: 98 | 99 | stream = io.BytesIO(bytes.fromhex(item["data"])) 100 | unmarshaller = Unmarshaller(stream) 101 | try: 102 | unmarshaller.unmarshall() 103 | except Exception as e: 104 | print("message failed to unmarshall:") 105 | print(json_dump(item["message"])) 106 | raise e 107 | 108 | message = json_to_message(item["message"]) 109 | 110 | body = [] 111 | for i, type_ in enumerate(message.signature_tree.types): 112 | body.append(replace_variants(type_, message.body[i])) 113 | message.body = body 114 | 115 | for attr in [ 116 | "body", 117 | "signature", 118 | "message_type", 119 | "destination", 120 | "path", 121 | "interface", 122 | "member", 123 | "flags", 124 | "serial", 125 | ]: 126 | assert getattr(unmarshaller.message, 127 | attr) == getattr(message, attr), f"attr doesnt match: {attr}" 128 | 129 | 130 | def test_unmarshall_can_resume(): 131 | """Verify resume works.""" 132 | bluez_rssi_message = ( 133 | "6c04010134000000e25389019500000001016f00250000002f6f72672f626c75657a2f686369302f6465" 134 | "765f30385f33415f46325f31455f32425f3631000000020173001f0000006f72672e667265656465736b" 135 | "746f702e444275732e50726f7065727469657300030173001100000050726f706572746965734368616e" 136 | "67656400000000000000080167000873617b73767d617300000007017300040000003a312e3400000000" 137 | "110000006f72672e626c75657a2e446576696365310000000e0000000000000004000000525353490001" 138 | "6e00a7ff000000000000") 139 | message_bytes = bytes.fromhex(bluez_rssi_message) 140 | 141 | class SlowStream(io.IOBase): 142 | """A fake stream that will only give us one byte at a time.""" 143 | def __init__(self): 144 | self.data = message_bytes 145 | self.pos = 0 146 | 147 | def read(self, n) -> bytes: 148 | data = self.data[self.pos:self.pos + 1] 149 | self.pos += 1 150 | return data 151 | 152 | stream = SlowStream() 153 | unmarshaller = Unmarshaller(stream) 154 | 155 | for _ in range(len(bluez_rssi_message)): 156 | if unmarshaller.unmarshall(): 157 | break 158 | assert unmarshaller.message is not None 159 | 160 | 161 | def test_ay_buffer(): 162 | body = [bytes(10000)] 163 | msg = Message(path="/test", member="test", signature="ay", body=body) 164 | marshalled = msg._marshall() 165 | unmarshalled_msg = Unmarshaller(io.BytesIO(marshalled)).unmarshall() 166 | assert unmarshalled_msg.body[0] == body[0] 167 | -------------------------------------------------------------------------------- /test/test_request_name.py: -------------------------------------------------------------------------------- 1 | from dbus_next import aio, glib, Message, MessageType, NameFlag, RequestNameReply, ReleaseNameReply 2 | from test.util import check_gi_repository, skip_reason_no_gi 3 | 4 | import pytest 5 | 6 | has_gi = check_gi_repository() 7 | 8 | 9 | @pytest.mark.asyncio 10 | async def test_name_requests(): 11 | test_name = 'aio.test.request.name' 12 | 13 | bus1 = await aio.MessageBus().connect() 14 | bus2 = await aio.MessageBus().connect() 15 | 16 | async def get_name_owner(name): 17 | reply = await bus1.call( 18 | Message(destination='org.freedesktop.DBus', 19 | path='/org/freedesktop/DBus', 20 | interface='org.freedesktop.DBus', 21 | member='GetNameOwner', 22 | signature='s', 23 | body=[name])) 24 | 25 | assert reply.message_type == MessageType.METHOD_RETURN 26 | return reply.body[0] 27 | 28 | reply = await bus1.request_name(test_name) 29 | assert reply == RequestNameReply.PRIMARY_OWNER 30 | reply = await bus1.request_name(test_name) 31 | assert reply == RequestNameReply.ALREADY_OWNER 32 | 33 | reply = await bus2.request_name(test_name, NameFlag.ALLOW_REPLACEMENT) 34 | assert reply == RequestNameReply.IN_QUEUE 35 | 36 | reply = await bus1.release_name(test_name) 37 | assert reply == ReleaseNameReply.RELEASED 38 | 39 | reply = await bus1.release_name('name.doesnt.exist') 40 | assert reply == ReleaseNameReply.NON_EXISTENT 41 | 42 | reply = await bus1.release_name(test_name) 43 | assert reply == ReleaseNameReply.NOT_OWNER 44 | 45 | new_owner = await get_name_owner(test_name) 46 | assert new_owner == bus2.unique_name 47 | 48 | reply = await bus1.request_name(test_name, NameFlag.DO_NOT_QUEUE) 49 | assert reply == RequestNameReply.EXISTS 50 | 51 | reply = await bus1.request_name(test_name, NameFlag.DO_NOT_QUEUE | NameFlag.REPLACE_EXISTING) 52 | assert reply == RequestNameReply.PRIMARY_OWNER 53 | 54 | bus1.disconnect() 55 | bus2.disconnect() 56 | 57 | 58 | @pytest.mark.skipif(not has_gi, reason=skip_reason_no_gi) 59 | def test_request_name_glib(): 60 | test_name = 'glib.test.request.name' 61 | bus = glib.MessageBus().connect_sync() 62 | 63 | reply = bus.request_name_sync(test_name) 64 | assert reply == RequestNameReply.PRIMARY_OWNER 65 | 66 | reply = bus.release_name_sync(test_name) 67 | assert reply == ReleaseNameReply.RELEASED 68 | 69 | bus.disconnect() 70 | -------------------------------------------------------------------------------- /test/test_signature.py: -------------------------------------------------------------------------------- 1 | from dbus_next import SignatureTree, SignatureBodyMismatchError, Variant 2 | from dbus_next._private.util import signature_contains_type 3 | 4 | import pytest 5 | 6 | 7 | def assert_simple_type(signature, type_): 8 | assert type_.token == signature 9 | assert type_.signature == signature 10 | assert len(type_.children) == 0 11 | 12 | 13 | def test_simple(): 14 | tree = SignatureTree('s') 15 | assert len(tree.types) == 1 16 | assert_simple_type('s', tree.types[0]) 17 | 18 | 19 | def test_multiple_simple(): 20 | tree = SignatureTree('sss') 21 | assert len(tree.types) == 3 22 | for i in range(0, 3): 23 | assert_simple_type('s', tree.types[i]) 24 | 25 | 26 | def test_array(): 27 | tree = SignatureTree('as') 28 | assert len(tree.types) == 1 29 | child = tree.types[0] 30 | assert child.signature == 'as' 31 | assert child.token == 'a' 32 | assert len(child.children) == 1 33 | assert_simple_type('s', child.children[0]) 34 | 35 | 36 | def test_array_multiple(): 37 | tree = SignatureTree('asasass') 38 | assert len(tree.types) == 4 39 | assert_simple_type('s', tree.types[3]) 40 | for i in range(0, 3): 41 | array_child = tree.types[i] 42 | assert array_child.token == 'a' 43 | assert array_child.signature == 'as' 44 | assert len(array_child.children) == 1 45 | assert_simple_type('s', array_child.children[0]) 46 | 47 | 48 | def test_array_nested(): 49 | tree = SignatureTree('aas') 50 | assert len(tree.types) == 1 51 | child = tree.types[0] 52 | assert child.token == 'a' 53 | assert child.signature == 'aas' 54 | assert len(child.children) == 1 55 | nested_child = child.children[0] 56 | assert nested_child.token == 'a' 57 | assert nested_child.signature == 'as' 58 | assert len(nested_child.children) == 1 59 | assert_simple_type('s', nested_child.children[0]) 60 | 61 | 62 | def test_simple_struct(): 63 | tree = SignatureTree('(sss)') 64 | assert len(tree.types) == 1 65 | child = tree.types[0] 66 | assert child.signature == '(sss)' 67 | assert len(child.children) == 3 68 | for i in range(0, 3): 69 | assert_simple_type('s', child.children[i]) 70 | 71 | 72 | def test_nested_struct(): 73 | tree = SignatureTree('(s(s(s)))') 74 | assert len(tree.types) == 1 75 | child = tree.types[0] 76 | assert child.signature == '(s(s(s)))' 77 | assert child.token == '(' 78 | assert len(child.children) == 2 79 | assert_simple_type('s', child.children[0]) 80 | first_nested = child.children[1] 81 | assert first_nested.token == '(' 82 | assert first_nested.signature == '(s(s))' 83 | assert len(first_nested.children) == 2 84 | assert_simple_type('s', first_nested.children[0]) 85 | second_nested = first_nested.children[1] 86 | assert second_nested.token == '(' 87 | assert second_nested.signature == '(s)' 88 | assert len(second_nested.children) == 1 89 | assert_simple_type('s', second_nested.children[0]) 90 | 91 | 92 | def test_struct_multiple(): 93 | tree = SignatureTree('(s)(s)(s)') 94 | assert len(tree.types) == 3 95 | for i in range(0, 3): 96 | child = tree.types[0] 97 | assert child.token == '(' 98 | assert child.signature == '(s)' 99 | assert len(child.children) == 1 100 | assert_simple_type('s', child.children[0]) 101 | 102 | 103 | def test_array_of_structs(): 104 | tree = SignatureTree('a(ss)') 105 | assert len(tree.types) == 1 106 | child = tree.types[0] 107 | assert child.token == 'a' 108 | assert child.signature == 'a(ss)' 109 | assert len(child.children) == 1 110 | struct_child = child.children[0] 111 | assert struct_child.token == '(' 112 | assert struct_child.signature == '(ss)' 113 | assert len(struct_child.children) == 2 114 | for i in range(0, 2): 115 | assert_simple_type('s', struct_child.children[i]) 116 | 117 | 118 | def test_dict_simple(): 119 | tree = SignatureTree('a{ss}') 120 | assert len(tree.types) == 1 121 | child = tree.types[0] 122 | assert child.signature == 'a{ss}' 123 | assert child.token == 'a' 124 | assert len(child.children) == 1 125 | dict_child = child.children[0] 126 | assert dict_child.token == '{' 127 | assert dict_child.signature == '{ss}' 128 | assert len(dict_child.children) == 2 129 | assert_simple_type('s', dict_child.children[0]) 130 | assert_simple_type('s', dict_child.children[1]) 131 | 132 | 133 | def test_dict_of_structs(): 134 | tree = SignatureTree('a{s(ss)}') 135 | assert len(tree.types) == 1 136 | child = tree.types[0] 137 | assert child.token == 'a' 138 | assert child.signature == 'a{s(ss)}' 139 | assert len(child.children) == 1 140 | dict_child = child.children[0] 141 | assert dict_child.token == '{' 142 | assert dict_child.signature == '{s(ss)}' 143 | assert len(dict_child.children) == 2 144 | assert_simple_type('s', dict_child.children[0]) 145 | struct_child = dict_child.children[1] 146 | assert struct_child.token == '(' 147 | assert struct_child.signature == '(ss)' 148 | assert len(struct_child.children) == 2 149 | for i in range(0, 2): 150 | assert_simple_type('s', struct_child.children[i]) 151 | 152 | 153 | def test_contains_type(): 154 | tree = SignatureTree('h') 155 | assert signature_contains_type(tree, [0], 'h') 156 | assert not signature_contains_type(tree, [0], 'u') 157 | 158 | tree = SignatureTree('ah') 159 | assert signature_contains_type(tree, [[0]], 'h') 160 | assert signature_contains_type(tree, [[0]], 'a') 161 | assert not signature_contains_type(tree, [[0]], 'u') 162 | 163 | tree = SignatureTree('av') 164 | body = [[Variant('u', 0), Variant('i', 0), Variant('x', 0), Variant('v', Variant('s', 'hi'))]] 165 | assert signature_contains_type(tree, body, 'u') 166 | assert signature_contains_type(tree, body, 'x') 167 | assert signature_contains_type(tree, body, 'v') 168 | assert signature_contains_type(tree, body, 's') 169 | assert not signature_contains_type(tree, body, 'o') 170 | 171 | tree = SignatureTree('a{sv}') 172 | body = { 173 | 'foo': Variant('h', 0), 174 | 'bar': Variant('i', 0), 175 | 'bat': Variant('x', 0), 176 | 'baz': Variant('v', Variant('o', '/hi')) 177 | } 178 | for expected in 'hixvso': 179 | assert signature_contains_type(tree, [body], expected) 180 | assert not signature_contains_type(tree, [body], 'b') 181 | 182 | 183 | def test_invalid_variants(): 184 | tree = SignatureTree('a{sa{sv}}') 185 | s_con = { 186 | 'type': '802-11-wireless', 187 | 'uuid': '1234', 188 | 'id': 'SSID', 189 | } 190 | 191 | s_wifi = { 192 | 'ssid': 'SSID', 193 | 'mode': 'infrastructure', 194 | 'hidden': True, 195 | } 196 | 197 | s_wsec = { 198 | 'key-mgmt': 'wpa-psk', 199 | 'auth-alg': 'open', 200 | 'psk': 'PASSWORD', 201 | } 202 | 203 | s_ip4 = {'method': 'auto'} 204 | s_ip6 = {'method': 'auto'} 205 | 206 | con = { 207 | 'connection': s_con, 208 | '802-11-wireless': s_wifi, 209 | '802-11-wireless-security': s_wsec, 210 | 'ipv4': s_ip4, 211 | 'ipv6': s_ip6 212 | } 213 | 214 | with pytest.raises(SignatureBodyMismatchError): 215 | tree.verify([con]) 216 | -------------------------------------------------------------------------------- /test/test_tcp_address.py: -------------------------------------------------------------------------------- 1 | from dbus_next.aio import MessageBus 2 | from dbus_next import Message 3 | from dbus_next._private.address import parse_address 4 | 5 | import asyncio 6 | import pytest 7 | import os 8 | 9 | 10 | @pytest.mark.asyncio 11 | async def test_tcp_connection_with_forwarding(event_loop): 12 | closables = [] 13 | host = '127.0.0.1' 14 | port = '55556' 15 | 16 | addr_info = parse_address(os.environ.get('DBUS_SESSION_BUS_ADDRESS')) 17 | assert addr_info 18 | assert 'abstract' in addr_info[0][1] 19 | path = f'\0{addr_info[0][1]["abstract"]}' 20 | 21 | async def handle_connection(tcp_reader, tcp_writer): 22 | unix_reader, unix_writer = await asyncio.open_unix_connection(path) 23 | closables.append(tcp_writer) 24 | closables.append(unix_writer) 25 | 26 | async def handle_read(): 27 | while True: 28 | data = await tcp_reader.read(1) 29 | if not data: 30 | break 31 | unix_writer.write(data) 32 | 33 | async def handle_write(): 34 | while True: 35 | data = await unix_reader.read(1) 36 | if not data: 37 | break 38 | tcp_writer.write(data) 39 | 40 | asyncio.run_coroutine_threadsafe(handle_read(), event_loop) 41 | asyncio.run_coroutine_threadsafe(handle_write(), event_loop) 42 | 43 | server = await asyncio.start_server(handle_connection, host, port) 44 | closables.append(server) 45 | 46 | bus = await MessageBus(bus_address=f'tcp:host={host},port={port}').connect() 47 | 48 | # basic tests to see if it works 49 | result = await bus.call( 50 | Message(destination='org.freedesktop.DBus', 51 | path='/org/freedesktop/DBus', 52 | interface='org.freedesktop.DBus.Peer', 53 | member='Ping')) 54 | assert result 55 | 56 | intr = await bus.introspect('org.freedesktop.DBus', '/org/freedesktop/DBus') 57 | obj = bus.get_proxy_object('org.freedesktop.DBus', '/org/freedesktop/DBus', intr) 58 | iface = obj.get_interface('org.freedesktop.DBus.Peer') 59 | await iface.call_ping() 60 | 61 | assert bus._sock.getpeername()[0] == host 62 | assert bus._sock.getsockname()[0] == host 63 | assert bus._sock.gettimeout() == 0 64 | assert bus._stream.closed is False 65 | 66 | for c in closables: 67 | c.close() 68 | -------------------------------------------------------------------------------- /test/test_validators.py: -------------------------------------------------------------------------------- 1 | from dbus_next import (is_bus_name_valid, is_object_path_valid, is_interface_name_valid, 2 | is_member_name_valid) 3 | 4 | 5 | def test_object_path_validator(): 6 | valid_paths = ['/', '/foo', '/foo/bar', '/foo/bar/bat'] 7 | invalid_paths = [ 8 | None, '', 'foo', 'foo/bar', '/foo/bar/', '/$/foo/bar', '/foo//bar', '/foo$bar/baz' 9 | ] 10 | 11 | for path in valid_paths: 12 | assert is_object_path_valid(path), f'path should be valid: "{path}"' 13 | for path in invalid_paths: 14 | assert not is_object_path_valid(path), f'path should be invalid: "{path}"' 15 | 16 | 17 | def test_bus_name_validator(): 18 | valid_names = [ 19 | 'foo.bar', 'foo.bar.bat', '_foo._bar', 'foo.bar69', 'foo.bar-69', 20 | 'org.mpris.MediaPlayer2.google-play-desktop-player' 21 | ] 22 | invalid_names = [ 23 | None, '', '5foo.bar', 'foo.6bar', '.foo.bar', 'bar..baz', '$foo.bar', 'foo$.ba$r' 24 | ] 25 | 26 | for name in valid_names: 27 | assert is_bus_name_valid(name), f'bus name should be valid: "{name}"' 28 | for name in invalid_names: 29 | assert not is_bus_name_valid(name), f'bus name should be invalid: "{name}"' 30 | 31 | 32 | def test_interface_name_validator(): 33 | valid_names = ['foo.bar', 'foo.bar.bat', '_foo._bar', 'foo.bar69'] 34 | invalid_names = [ 35 | None, '', '5foo.bar', 'foo.6bar', '.foo.bar', 'bar..baz', '$foo.bar', 'foo$.ba$r', 36 | 'org.mpris.MediaPlayer2.google-play-desktop-player' 37 | ] 38 | 39 | for name in valid_names: 40 | assert is_interface_name_valid(name), f'interface name should be valid: "{name}"' 41 | for name in invalid_names: 42 | assert not is_interface_name_valid(name), f'interface name should be invalid: "{name}"' 43 | 44 | 45 | def test_member_name_validator(): 46 | valid_members = ['foo', 'FooBar', 'Bat_Baz69', 'foo-bar'] 47 | invalid_members = [None, '', 'foo.bar', '5foo', 'foo$bar'] 48 | 49 | for member in valid_members: 50 | assert is_member_name_valid(member), f'member name should be valid: "{member}"' 51 | for member in invalid_members: 52 | assert not is_member_name_valid(member), f'member name should be invalid: "{member}"' 53 | -------------------------------------------------------------------------------- /test/util.py: -------------------------------------------------------------------------------- 1 | _has_gi = None 2 | skip_reason_no_gi = 'glib tests require python3-gi' 3 | 4 | 5 | def check_gi_repository(): 6 | global _has_gi 7 | if _has_gi is not None: 8 | return _has_gi 9 | try: 10 | from gi.repository import GLib 11 | _has_gi = True 12 | return _has_gi 13 | except ImportError: 14 | _has_gi = False 15 | return _has_gi 16 | --------------------------------------------------------------------------------