pax_global_header00006660000000000000000000000064140773577270014535gustar00rootroot0000000000000052 comment=74dc9706e8d0ebb17f27818b8ef9e214172514ec python-dbus-next-0.2.3/000077500000000000000000000000001407735772700147675ustar00rootroot00000000000000python-dbus-next-0.2.3/.flake8000066400000000000000000000003571407735772700161470ustar00rootroot00000000000000[flake8] ignore= E501 E126 E402 F722 # F821 is still relevant, but causes too many false positives in tests and # examples per-file-ignores= test/*:F821 test/util.py:F401 examples/*:F821 */__init__.py:F401 python-dbus-next-0.2.3/.gitignore000066400000000000000000000032551407735772700167640ustar00rootroot00000000000000# Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ pip-wheel-metadata/ share/python-wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover .hypothesis/ .pytest_cache/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # Jupyter Notebook .ipynb_checkpoints # IPython profile_default/ ipython_config.py # pyenv .python-version # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. # However, in case of collaboration, if having platform-specific dependencies or dependencies # having no cross-platform support, pipenv may install dependencies that don’t work, or not # install all needed dependencies. #Pipfile.lock # celery beat schedule file celerybeat-schedule # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ .dmypy.json dmypy.json # Pyre type checker .pyre/ # vim *.swp python-dbus-next-0.2.3/.style.yapf000066400000000000000000000000331407735772700170620ustar00rootroot00000000000000[style] column_limit = 100 python-dbus-next-0.2.3/.travis.yml000066400000000000000000000002421407735772700170760ustar00rootroot00000000000000language: minimal sudo: required dist: xenial services: - docker before_install: - docker build -t dbus-next-test . script: - docker run -it dbus-next-test python-dbus-next-0.2.3/CHANGELOG.md000066400000000000000000000052301407735772700166000ustar00rootroot00000000000000# Changelog ## Version 0.2.3 This version contains some bugfixes and new features. * Include py.typed in Manifest.in (#79) * Fix property validation error message (#81) * Don't log errors if call failed after disconnect (#84) * Support PEP563 annotations (#3) * Service: support async properties (#86) * Client: Support coroutines as signal handlers (#2) ## Version 0.2.2 This version contains some bugfixes and a new feature * Add `connected` instance variable to the `MessageBus` (#74) * Better handling of message bus errors on disconnect (de8ed30) * Ensure futures are not done when settings results and exceptions (#73, 1213667) ## Version 0.2.1 This version adds performance optimizations, bugfixes, and new features. * aio.MessageBus: Support passing unix fds. (#54) * Unmarshaller optimizations for a significant performance increase in message reading. (#62, #64) * Cache instances of `SignatureTree`. (ace5584) * Fix socket creation on macos. (#63) * Implement PEP 561 to indicate inline type hints. (#69) * aio.MessageBus: Return a future from `send()`. (302511b) * aio.MessageBus: Add `wait_for_disconnect()` to detect connection errors. (ab01ab1) Notice: `aio.MessageBus.send()` will be changed to a coroutine function in the 1.0 version of this library. ## Version 0.1.4 This version adds some bugfixes and new features. * Support tcp transport addresses (#57) * Add support for the annonymous authentication protocol (#32) * Add flags kwarg to aio high level client method call (#55) * Allow subclassing of DBusError (#42) * Fix exception in aio message handler loop on task cancellation (ff165aa) * Improve error messages (#46, #59) * Fix match rule memory leak bug (508edf8) * Don't add match rules for high level client by default (615218f) * Add empty properties interface to standard interfaces (#49) ## Version 0.1.3 This version adds some bugfixes and new features. * Add the object manager interface to the service. (#14, #37) * Allow coroutines in service methods. (#24, #27) * Client: don't send method replies with `NO_REPLY_EXPECTED` message flag. (#22) * Fix duplicate nodes in introspection. (#13) ## Version 0.1.2 This version adds some bugfixes. * Allow exporting interface multiple times (#4) * Fix super call in exceptions (#5) * Add timeout support on `introspect` (#7) * Add unix fd type 'h' to valid tokens (#9) * Dont use future annotations (#10) * Fix variant validator (d724fc2) ## Version 0.1.1 This version adds some major features and breaking changes. * Remove the MessageBus convenience constructors (breaking). * Complete documentation. * Type annotation for all public methods. ## Version 0.0.1 This is the first release of python-dbus-next. python-dbus-next-0.2.3/Dockerfile000066400000000000000000000022251407735772700167620ustar00rootroot00000000000000FROM ubuntu:20.04 WORKDIR /app RUN echo force-unsafe-io > /etc/dpkg/dpkg.cfg.d/docker-apt-speedup RUN echo 'APT::Acquire::Retries "5";' > /etc/apt/apt.conf.d/80retry RUN export DEBIAN_FRONTEND=noninteractive; \ export DEBCONF_NONINTERACTIVE_SEEN=true; \ echo 'tzdata tzdata/Areas select Etc' | debconf-set-selections; \ echo 'tzdata tzdata/Zones/Etc select UTC' | debconf-set-selections; \ apt update && \ apt install software-properties-common -y --no-install-recommends && \ add-apt-repository ppa:deadsnakes/ppa && \ apt update && apt install -y --no-install-recommends \ build-essential \ python3-pip \ python3 \ python3.6 \ python3.7 \ python3.9 \ python3.9-distutils \ python3.10 \ python3.10-distutils \ curl \ dbus \ python3-gi RUN pip3 install yapf flake8 && \ for py in python3 python3.6 python3.7 python3.9 python3.10; do \ curl https://bootstrap.pypa.io/get-pip.py | $py; \ $py -m pip install \ pytest \ pytest-asyncio \ pytest-timeout \ pytest-cov; \ done ADD . /app CMD ["make", "clean", "test", "check"] python-dbus-next-0.2.3/LICENSE000066400000000000000000000020371407735772700157760ustar00rootroot00000000000000Copyright (c) 2019 Tony Crisci Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. python-dbus-next-0.2.3/MANIFEST.in000066400000000000000000000002211407735772700165200ustar00rootroot00000000000000include README.md LICENSE CHANGELOG.md pytest.ini requirements.txt .flake8 .style.yapf Dockerfile dbus_next/py.typed recursive-include test *.py python-dbus-next-0.2.3/Makefile000066400000000000000000000015771407735772700164410ustar00rootroot00000000000000.PHONY: lint check format test docker-test clean publish docs livedocs all .DEFAULT_GOAL := all source_dirs = dbus_next test examples lint: python3 -m flake8 $(source_dirs) check: lint python3 -m yapf -rdp $(source_dirs) format: python3 -m yapf -rip $(source_dirs) test: for py in python3.6 python3.7 python3.9 python3.10 python3.8 ; do \ if hash $$py; then \ dbus-run-session $$py -m pytest -sv --cov=dbus_next || exit 1 ; \ fi \ done \ docker-test: docker build -t dbus-next-test . docker run -it dbus-next-test clean: rm -rf dist dbus_next.egg-info build docs/_build rm -rf `find -type d -name __pycache__` publish: python3 setup.py sdist bdist_wheel python3 -m twine upload --repository-url https://upload.pypi.org/legacy/ dist/* docs: sphinx-build docs docs/_build/html livedocs: sphinx-autobuild docs docs/_build/html --watch dbus_next all: format lint test python-dbus-next-0.2.3/README.md000066400000000000000000000135011407735772700162460ustar00rootroot00000000000000# python-dbus-next The next great DBus library for Python. [Documentation](https://python-dbus-next.readthedocs.io/en/latest/) [Chat](https://discord.gg/UdbXHVX) 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. 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. Desktop users can use this library to create their own scripts and utilities to interact with those interfaces for customization of their desktop environment. python-dbus-next plans to improve over other DBus libraries for Python in the following ways: * Zero dependencies and pure Python 3. * Support for multiple IO backends including asyncio and the GLib main loop. * Nonblocking IO suitable for GUI development. * Target the latest language features of Python for beautiful services and clients. * Complete implementation of the DBus type system without ever guessing types. * Integration tests for all features of the library. * Completely documented public API. ## Installing This library is available on PyPi as [dbus-next](https://pypi.org/project/dbus-next/). ``` pip3 install dbus-next ``` ## The Client Interface 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. For more information, see the [overview for the high-level client](https://python-dbus-next.readthedocs.io/en/latest/high-level-client/index.html). This example connects to a media player and controls it with the [MPRIS](https://specifications.freedesktop.org/mpris-spec/latest/) DBus interface. ```python from dbus_next.aio import MessageBus import asyncio loop = asyncio.get_event_loop() async def main(): bus = await MessageBus().connect() # the introspection xml would normally be included in your project, but # this is convenient for development introspection = await bus.introspect('org.mpris.MediaPlayer2.vlc', '/org/mpris/MediaPlayer2') obj = bus.get_proxy_object('org.mpris.MediaPlayer2.vlc', '/org/mpris/MediaPlayer2', introspection) player = obj.get_interface('org.mpris.MediaPlayer2.Player') properties = obj.get_interface('org.freedesktop.DBus.Properties') # call methods on the interface (this causes the media player to play) await player.call_play() volume = await player.get_volume() print(f'current volume: {volume}, setting to 0.5') await player.set_volume(0.5) # listen to signals def on_properties_changed(interface_name, changed_properties, invalidated_properties): for changed, variant in changed_properties.items(): print(f'property changed: {changed} - {variant.value}') properties.on_properties_changed(on_properties_changed) await loop.create_future() loop.run_until_complete(main()) ``` ## The Service Interface 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. For more information, see the [overview for the high-level service](https://python-dbus-next.readthedocs.io/en/latest/high-level-service/index.html). ```python from dbus_next.service import ServiceInterface, method, dbus_property, signal, Variant from dbus_next.aio MessageBus import asyncio class ExampleInterface(ServiceInterface): def __init__(self, name): super().__init__(name) self._string_prop = 'kevin' @method() def Echo(self, what: 's') -> 's': return what @method() def GetVariantDict() -> 'a{sv}': return { 'foo': Variant('s', 'bar'), 'bat': Variant('x', -55), 'a_list': Variant('as', ['hello', 'world']) } @dbus_property() def string_prop(self) -> 's': return self._string_prop @string_prop.setter def string_prop_setter(self, val: 's'): self._string_prop = val @signal() def signal_simple(self) -> 's': return 'hello' async def main(): bus = await MessageBus().connect() interface = ExampleInterface('test.interface') bus.export('/test/path', interface) # now that we are ready to handle requests, we can request name from D-Bus await bus.request_name('test.name') # wait indefinitely await asyncio.get_event_loop().create_future() asyncio.get_event_loop().run_until_complete(main()) ``` ## The Low-Level Interface The low-level interface works with DBus messages directly. For more information, see the [overview for the low-level interface](https://python-dbus-next.readthedocs.io/en/latest/low-level-interface/index.html). ```python from dbus_next.message import Message, MessageType from dbus_next.aio import MessageBus import asyncio import json loop = asyncio.get_event_loop() async def main(): bus = await MessageBus().connect() reply = await bus.call( Message(destination='org.freedesktop.DBus', path='/org/freedesktop/DBus', interface='org.freedesktop.DBus', member='ListNames')) if reply.message_type == MessageType.ERROR: raise Exception(reply.body[0]) print(json.dumps(reply.body[0], indent=2)) loop.run_until_complete(main()) ``` ## Projects that use python-dbus-next * The [Playerctl](https://github.com/altdesktop/playerctl) test suite * [i3-dstatus](https://github.com/altdesktop/i3-dstatus) ## Contributing Contributions are welcome. Development happens on [Github](https://github.com/altdesktop/python-dbus-next). Before you commit, run `make` to run the linter, code formatter, and the test suite. # Copyright You can use this code under an MIT license (see LICENSE). © 2019, Tony Crisci python-dbus-next-0.2.3/dbus_next/000077500000000000000000000000001407735772700167625ustar00rootroot00000000000000python-dbus-next-0.2.3/dbus_next/__init__.py000066400000000000000000000020371407735772700210750ustar00rootroot00000000000000from . import aio from . import glib from .constants import (BusType, MessageType, MessageFlag, NameFlag, RequestNameReply, ReleaseNameReply, PropertyAccess, ArgDirection, ErrorType) from .errors import (SignatureBodyMismatchError, InvalidSignatureError, InvalidAddressError, AuthError, InvalidMessageError, InvalidIntrospectionError, InterfaceNotFoundError, SignalDisabledError, InvalidBusNameError, InvalidObjectPathError, InvalidInterfaceNameError, InvalidMemberNameError, DBusError) from . import introspection from .message import Message from . import message_bus from . import proxy_object from . import service from .signature import SignatureType, SignatureTree, Variant from .validators import (is_bus_name_valid, is_object_path_valid, is_interface_name_valid, is_member_name_valid, assert_bus_name_valid, assert_object_path_valid, assert_interface_name_valid, assert_member_name_valid) python-dbus-next-0.2.3/dbus_next/__version__.py000066400000000000000000000005021407735772700216120ustar00rootroot00000000000000__title__ = 'dbus_next' __description__ = 'A zero-dependency DBus library for Python with asyncio support' __url__ = 'https://github.com/altdesktop/python-dbus-next' __version__ = '0.2.3' __author__ = 'Tony Crisci' __author_email__ = 'tony@dubstepdish.com' __license__ = 'MIT' __copyright__ = 'Copyright 2019 Tony Crisci' python-dbus-next-0.2.3/dbus_next/_private/000077500000000000000000000000001407735772700205735ustar00rootroot00000000000000python-dbus-next-0.2.3/dbus_next/_private/__init__.py000066400000000000000000000000001407735772700226720ustar00rootroot00000000000000python-dbus-next-0.2.3/dbus_next/_private/address.py000066400000000000000000000066251407735772700226030ustar00rootroot00000000000000from ..constants import BusType from ..errors import InvalidAddressError from urllib.parse import unquote import re import os invalid_address_chars_re = re.compile(r'[^-0-9A-Za-z_/.%]') def parse_address(address_str): addresses = [] for address in filter(lambda a: a, address_str.split(';')): if address.find(':') == -1: raise InvalidAddressError('address did not contain a transport') transport, opt_string = address.split(':', 1) options = {} for kv in filter(lambda s: s, opt_string.split(',')): if kv.find('=') == -1: raise InvalidAddressError('address option did not contain a value') k, v = kv.split('=', 1) if invalid_address_chars_re.search(v): raise InvalidAddressError('address contains invalid characters') # XXX the actual unquote rules are simpler than this v = unquote(v) options[k] = v addresses.append((transport, options)) if not addresses: raise InvalidAddressError(f'address string contained no addresses: "{address_str}"') return addresses def get_system_bus_address(): if 'DBUS_SYSTEM_BUS_ADDRESS' in os.environ: return os.environ['DBUS_SYSTEM_BUS_ADDRESS'] else: return 'unix:path=/var/run/dbus/system_bus_socket' display_re = re.compile(r'.*:([0-9]+)\.?.*') remove_quotes_re = re.compile(r'''^['"]?(.*?)['"]?$''') def get_session_bus_address(): if 'DBUS_SESSION_BUS_ADDRESS' in os.environ: return os.environ['DBUS_SESSION_BUS_ADDRESS'] home = os.environ['HOME'] if 'DISPLAY' not in os.environ: raise InvalidAddressError( 'DBUS_SESSION_BUS_ADDRESS not set and could not get DISPLAY environment variable to get bus address' ) display = os.environ['DISPLAY'] try: display = display_re.search(display).group(1) except Exception: raise InvalidAddressError( f'DBUS_SESSION_BUS_ADDRESS not set and could not parse DISPLAY environment variable to get bus address: {display}' ) # XXX: this will block but they're very small files and fs operations # should be fairly reliable. fix this by passing in an async func to read # the file for each io backend. machine_id = None with open('/var/lib/dbus/machine-id') as f: machine_id = f.read().rstrip() dbus_info_file_name = f'{home}/.dbus/session-bus/{machine_id}-{display}' dbus_info = None try: with open(dbus_info_file_name) as f: dbus_info = f.read().rstrip() except Exception: raise InvalidAddressError(f'could not open dbus info file: {dbus_info_file_name}') for line in dbus_info.split('\n'): if line.strip().startswith('DBUS_SESSION_BUS_ADDRESS='): _, addr = line.split('=', 1) if not addr: raise InvalidAddressError( f'DBUS_SESSION_BUS_ADDRESS variable not set correctly in dbus info file: {dbus_info_file_name}' ) addr = remove_quotes_re.search(addr).group(1) return addr raise InvalidAddressError('could not find dbus session bus address') def get_bus_address(bus_type): if bus_type == BusType.SESSION: return get_session_bus_address() elif bus_type == BusType.SYSTEM: return get_system_bus_address() else: raise Exception('got unknown bus type: {bus_type}') python-dbus-next-0.2.3/dbus_next/_private/constants.py000066400000000000000000000004231407735772700231600ustar00rootroot00000000000000from enum import Enum PROTOCOL_VERSION = 1 LITTLE_ENDIAN = ord('l') BIG_ENDIAN = ord('B') class HeaderField(Enum): PATH = 1 INTERFACE = 2 MEMBER = 3 ERROR_NAME = 4 REPLY_SERIAL = 5 DESTINATION = 6 SENDER = 7 SIGNATURE = 8 UNIX_FDS = 9 python-dbus-next-0.2.3/dbus_next/_private/marshaller.py000066400000000000000000000116731407735772700233070ustar00rootroot00000000000000from ..signature import SignatureTree from struct import pack class Marshaller: def __init__(self, signature, body): self.signature_tree = SignatureTree._get(signature) self.signature_tree.verify(body) self.buffer = bytearray() self.body = body self.writers = { 'y': self.write_byte, 'b': self.write_boolean, 'n': self.write_int16, 'q': self.write_uint16, 'i': self.write_int32, 'u': self.write_uint32, 'x': self.write_int64, 't': self.write_uint64, 'd': self.write_double, 'h': self.write_uint32, 'o': self.write_string, 's': self.write_string, 'g': self.write_signature, 'a': self.write_array, '(': self.write_struct, '{': self.write_dict_entry, 'v': self.write_variant } def align(self, n): offset = n - len(self.buffer) % n if offset == 0 or offset == n: return 0 self.buffer.extend(bytes(offset)) return offset def write_byte(self, byte, _=None): self.buffer.append(byte) return 1 def write_boolean(self, boolean, _=None): if boolean: return self.write_uint32(1) else: return self.write_uint32(0) def write_int16(self, int16, _=None): written = self.align(2) self.buffer.extend(pack(' 0: data = read_sock(missing_bytes) if data == b'': raise EOFError() elif data is None: raise MarshallerStreamEndError() self.buf.extend(data) if len(data) != missing_bytes: raise MarshallerStreamEndError() prev = self.offset if not prefetch: self.offset += n return prev @staticmethod def _padding(offset, align): """ Get padding bytes to get to the next align bytes mark. For any align value, the correct padding formula is: (align - (offset % align)) % align However, if align is a power of 2 (always the case here), the slow MOD operator can be replaced by a bitwise AND: (align - (offset & (align - 1))) & (align - 1) Which can be simplified to: (-offset) & (align - 1) """ return (-offset) & (align - 1) def align(self, n): padding = self._padding(self.offset, n) if padding > 0: self.read(padding) def read_byte(self, _=None): return self.buf[self.read(1)] def read_boolean(self, _=None): data = self.read_uint32() if data: return True else: return False def read_int16(self, _=None): return self.read_ctype('h', 2) def read_uint16(self, _=None): return self.read_ctype('H', 2) def read_int32(self, _=None): return self.read_ctype('i', 4) def read_uint32(self, _=None): return self.read_ctype('I', 4) def read_int64(self, _=None): return self.read_ctype('q', 8) def read_uint64(self, _=None): return self.read_ctype('Q', 8) def read_double(self, _=None): return self.read_ctype('d', 8) def read_ctype(self, fmt, size): self.align(size) if self.endian == LITTLE_ENDIAN: fmt = '<' + fmt else: fmt = '>' + fmt o = self.read(size) return unpack_from(fmt, self.buf, o)[0] def read_string(self, _=None): str_length = self.read_uint32() o = self.read(str_length + 1) # read terminating '\0' byte as well # avoid buffer copies when slicing str_mem_slice = memoryview(self.buf)[o:o + str_length] return decode(str_mem_slice) def read_signature(self, _=None): signature_len = self.read_byte() o = self.read(signature_len + 1) # read terminating '\0' byte as well # avoid buffer copies when slicing sig_mem_slice = memoryview(self.buf)[o:o + signature_len] return decode(sig_mem_slice) def read_variant(self, _=None): signature = self.read_signature() signature_tree = SignatureTree._get(signature) value = self.read_argument(signature_tree.types[0]) return Variant(signature_tree, value) def read_struct(self, type_): self.align(8) result = [] for child_type in type_.children: result.append(self.read_argument(child_type)) return result def read_dict_entry(self, type_): self.align(8) key = self.read_argument(type_.children[0]) value = self.read_argument(type_.children[1]) return key, value def read_array(self, type_): self.align(4) array_length = self.read_uint32() child_type = type_.children[0] if child_type.token in 'xtd{(': # the first alignment is not included in the array size self.align(8) beginning_offset = self.offset result = None if child_type.token == '{': result = {} while self.offset - beginning_offset < array_length: key, value = self.read_dict_entry(child_type) result[key] = value elif child_type.token == 'y': o = self.read(array_length) # avoid buffer copies when slicing array_mem_slice = memoryview(self.buf)[o:o + array_length] result = array_mem_slice.tobytes() else: result = [] while self.offset - beginning_offset < array_length: result.append(self.read_argument(child_type)) return result def read_argument(self, type_): t = type_.token if t not in self.readers: raise Exception(f'dont know how to read yet: "{t}"') return self.readers[t](type_) def _unmarshall(self): self.offset = 0 self.read(16, prefetch=True) self.endian = self.read_byte() if self.endian != LITTLE_ENDIAN and self.endian != BIG_ENDIAN: raise InvalidMessageError('Expecting endianness as the first byte') message_type = MessageType(self.read_byte()) flags = MessageFlag(self.read_byte()) protocol_version = self.read_byte() if protocol_version != PROTOCOL_VERSION: raise InvalidMessageError(f'got unknown protocol version: {protocol_version}') body_len = self.read_uint32() serial = self.read_uint32() header_len = self.read_uint32() msg_len = header_len + self._padding(header_len, 8) + body_len self.read(msg_len, prefetch=True) # backtrack offset since header array length needs to be read again self.offset -= 4 header_fields = {} for field_struct in self.read_argument(SignatureTree._get('a(yv)').types[0]): field = HeaderField(field_struct[0]) header_fields[field.name] = field_struct[1].value self.align(8) path = header_fields.get(HeaderField.PATH.name) interface = header_fields.get(HeaderField.INTERFACE.name) member = header_fields.get(HeaderField.MEMBER.name) error_name = header_fields.get(HeaderField.ERROR_NAME.name) reply_serial = header_fields.get(HeaderField.REPLY_SERIAL.name) destination = header_fields.get(HeaderField.DESTINATION.name) sender = header_fields.get(HeaderField.SENDER.name) signature = header_fields.get(HeaderField.SIGNATURE.name, '') signature_tree = SignatureTree._get(signature) # unix_fds = header_fields.get(HeaderField.UNIX_FDS.name, 0) body = [] if body_len: for type_ in signature_tree.types: body.append(self.read_argument(type_)) self.message = Message(destination=destination, path=path, interface=interface, member=member, message_type=message_type, flags=flags, error_name=error_name, reply_serial=reply_serial, sender=sender, unix_fds=self.unix_fds, signature=signature_tree, body=body, serial=serial) def unmarshall(self): try: self._unmarshall() return self.message except MarshallerStreamEndError: return None python-dbus-next-0.2.3/dbus_next/_private/util.py000066400000000000000000000127661407735772700221360ustar00rootroot00000000000000from typing import List, Any, Union import inspect from ..signature import SignatureTree, Variant import ast def signature_contains_type(signature: Union[str, SignatureTree], body: List[Any], token: str) -> bool: '''For a given signature and body, check to see if it contains any members with the given token''' if type(signature) is str: signature = SignatureTree._get(signature) queue = [] contains_variants = False for st in signature.types: queue.append(st) while True: if not queue: break st = queue.pop() if st.token == token: return True elif st.token == 'v': contains_variants = True queue.extend(st.children) if not contains_variants: return False for member in body: queue.append(member) while True: if not queue: return False member = queue.pop() if type(member) is Variant and \ signature_contains_type(member.signature, [member.value], token): return True elif type(member) is list: queue.extend(member) elif type(member) is dict: queue.extend(member.values()) def replace_fds_with_idx(signature: Union[str, SignatureTree], body: List[Any]) -> (List[Any], List[int]): '''Take the high level body format and convert it into the low level body format. Type 'h' refers directly to the fd in the body. Replace that with an index and return the corresponding list of unix fds that can be set on the Message''' if type(signature) is str: signature = SignatureTree._get(signature) if not signature_contains_type(signature, body, 'h'): return body, [] unix_fds = [] def _replace(fd): try: return unix_fds.index(fd) except ValueError: unix_fds.append(fd) return len(unix_fds) - 1 _replace_fds(body, signature.types, _replace) return body, unix_fds def replace_idx_with_fds(signature: Union[str, SignatureTree], body: List[Any], unix_fds: List[int]) -> List[Any]: '''Take the low level body format and return the high level body format. Type 'h' refers to an index in the unix_fds array. Replace those with the actual file descriptor or `None` if one does not exist.''' if type(signature) is str: signature = SignatureTree._get(signature) if not signature_contains_type(signature, body, 'h'): return body def _replace(idx): try: return unix_fds[idx] except IndexError: return None _replace_fds(body, signature.types, _replace) return body def parse_annotation(annotation: str) -> str: ''' Because of PEP 563, if `from __future__ import annotations` is used in code or on Python version >=3.10 where this is the default, return annotations from the `inspect` module will return annotations as "forward definitions". In this case, we must eval the result which we do only when given a string constant. ''' def raise_value_error(): raise ValueError(f'service annotations must be a string constant (got {annotation})') if not annotation or annotation is inspect.Signature.empty: return '' if type(annotation) is not str: raise_value_error() try: body = ast.parse(annotation).body if len(body) == 1 and type(body[0].value) is ast.Constant: if type(body[0].value.value) is not str: raise_value_error() return body[0].value.value except SyntaxError: pass return annotation def _replace_fds(body_obj: List[Any], children, replace_fn): '''Replace any type 'h' with the value returned by replace_fn() given the value of the fd field. This is used by the high level interfaces which allow type 'h' to be the fd directly instead of an index in an external array such as in the spec.''' for index, st in enumerate(children): if not any(sig in st.signature for sig in 'hv'): continue if st.signature == 'h': body_obj[index] = replace_fn(body_obj[index]) elif st.token == 'a': if st.children[0].token == '{': _replace_fds(body_obj[index], st.children, replace_fn) else: for i, child in enumerate(body_obj[index]): if st.signature == 'ah': body_obj[index][i] = replace_fn(child) else: _replace_fds([child], st.children, replace_fn) elif st.token in '(': _replace_fds(body_obj[index], st.children, replace_fn) elif st.token in '{': for key, value in list(body_obj.items()): body_obj.pop(key) if st.children[0].signature == 'h': key = replace_fn(key) if st.children[1].signature == 'h': value = replace_fn(value) else: _replace_fds([value], [st.children[1]], replace_fn) body_obj[key] = value elif st.signature == 'v': if body_obj[index].signature == 'h': body_obj[index].value = replace_fn(body_obj[index].value) else: _replace_fds([body_obj[index].value], [body_obj[index].type], replace_fn) elif st.children: _replace_fds(body_obj[index], st.children, replace_fn) python-dbus-next-0.2.3/dbus_next/aio/000077500000000000000000000000001407735772700175325ustar00rootroot00000000000000python-dbus-next-0.2.3/dbus_next/aio/__init__.py000066400000000000000000000001321407735772700216370ustar00rootroot00000000000000from .message_bus import MessageBus from .proxy_object import ProxyObject, ProxyInterface python-dbus-next-0.2.3/dbus_next/aio/message_bus.py000066400000000000000000000363671407735772700224200ustar00rootroot00000000000000from ..message_bus import BaseMessageBus from .._private.unmarshaller import Unmarshaller from ..message import Message from ..constants import BusType, NameFlag, RequestNameReply, ReleaseNameReply, MessageType, MessageFlag from ..service import ServiceInterface from ..errors import AuthError from .proxy_object import ProxyObject from .. import introspection as intr from ..auth import Authenticator, AuthExternal import logging import array import asyncio from asyncio import Queue import socket from copy import copy from typing import Optional def _future_set_exception(fut, exc): if fut is not None and not fut.done(): fut.set_exception(exc) def _future_set_result(fut, result): if fut is not None and not fut.done(): fut.set_result(result) class _MessageWriter: def __init__(self, bus): self.messages = Queue() self.negotiate_unix_fd = bus._negotiate_unix_fd self.bus = bus self.sock = bus._sock self.loop = bus._loop self.buf = None self.fd = bus._fd self.offset = 0 self.unix_fds = None self.fut = None def write_callback(self): try: while True: if self.buf is None: if self.messages.qsize() == 0: # nothing more to write self.loop.remove_writer(self.fd) return buf, unix_fds, fut = self.messages.get_nowait() self.unix_fds = unix_fds self.buf = memoryview(buf) self.offset = 0 self.fut = fut if self.unix_fds and self.negotiate_unix_fd: ancdata = [(socket.SOL_SOCKET, socket.SCM_RIGHTS, array.array("i", self.unix_fds))] self.offset += self.sock.sendmsg([self.buf[self.offset:]], ancdata) self.unix_fds = None else: self.offset += self.sock.send(self.buf[self.offset:]) if self.offset >= len(self.buf): # finished writing self.buf = None _future_set_result(self.fut, None) else: # wait for writable return except Exception as e: _future_set_exception(self.fut, e) self.bus._finalize(e) def buffer_message(self, msg: Message, future=None): self.messages.put_nowait( (msg._marshall(negotiate_unix_fd=self.negotiate_unix_fd), copy(msg.unix_fds), future)) def schedule_write(self, msg: Message = None, future=None): if msg is not None: self.buffer_message(msg, future) if self.bus.unique_name: # don't run the writer until the bus is ready to send messages self.loop.add_writer(self.fd, self.write_callback) class MessageBus(BaseMessageBus): """The message bus implementation for use with asyncio. The message bus class is the entry point into all the features of the library. It sets up a connection to the DBus daemon and exposes an interface to send and receive messages and expose services. You must call :func:`connect() ` before using this message bus. :param bus_type: The type of bus to connect to. Affects the search path for the bus address. :type bus_type: :class:`BusType ` :param bus_address: A specific bus address to connect to. Should not be used under normal circumstances. :param auth: The authenticator to use, defaults to an instance of :class:`AuthExternal `. :type auth: :class:`Authenticator ` :param negotiate_unix_fd: Allow the bus to send and receive Unix file descriptors (DBus type 'h'). This must be supported by the transport. :type negotiate_unix_fd: bool :ivar unique_name: The unique name of the message bus connection. It will be :class:`None` until the message bus connects. :vartype unique_name: str :ivar connected: True if this message bus is expected to be able to send and receive messages. :vartype connected: bool """ def __init__(self, bus_address: str = None, bus_type: BusType = BusType.SESSION, auth: Authenticator = None, negotiate_unix_fd=False): super().__init__(bus_address, bus_type, ProxyObject) self._negotiate_unix_fd = negotiate_unix_fd self._loop = asyncio.get_event_loop() self._unmarshaller = self._create_unmarshaller() self._writer = _MessageWriter(self) if auth is None: self._auth = AuthExternal() else: self._auth = auth self._disconnect_future = self._loop.create_future() async def connect(self) -> 'MessageBus': """Connect this message bus to the DBus daemon. This method must be called before the message bus can be used. :returns: This message bus for convenience. :rtype: :class:`MessageBus ` :raises: - :class:`AuthError ` - If authorization to \ the DBus daemon failed. - :class:`Exception` - If there was a connection error. """ await self._authenticate() future = self._loop.create_future() self._loop.add_reader(self._fd, self._message_reader) def on_hello(reply, err): try: if err: raise err self.unique_name = reply.body[0] self._writer.schedule_write() _future_set_result(future, self) except Exception as e: _future_set_exception(future, e) self.disconnect() self._finalize(err) hello_msg = Message(destination='org.freedesktop.DBus', path='/org/freedesktop/DBus', interface='org.freedesktop.DBus', member='Hello', serial=self.next_serial()) self._method_return_handlers[hello_msg.serial] = on_hello self._stream.write(hello_msg._marshall()) self._stream.flush() return await future async def introspect(self, bus_name: str, path: str, timeout: float = 30.0) -> intr.Node: """Get introspection data for the node at the given path from the given bus name. Calls the standard ``org.freedesktop.DBus.Introspectable.Introspect`` on the bus for the path. :param bus_name: The name to introspect. :type bus_name: str :param path: The path to introspect. :type path: str :param timeout: The timeout to introspect. :type timeout: float :returns: The introspection data for the name at the path. :rtype: :class:`Node ` :raises: - :class:`InvalidObjectPathError ` \ - If the given object path is not valid. - :class:`InvalidBusNameError ` - If \ the given bus name is not valid. - :class:`DBusError ` - If the service threw \ an error for the method call or returned an invalid result. - :class:`Exception` - If a connection error occurred. - :class:`asyncio.TimeoutError` - Waited for future but time run out. """ future = self._loop.create_future() def reply_handler(reply, err): if err: _future_set_exception(future, err) else: _future_set_result(future, reply) super().introspect(bus_name, path, reply_handler) return await asyncio.wait_for(future, timeout=timeout) async def request_name(self, name: str, flags: NameFlag = NameFlag.NONE) -> RequestNameReply: """Request that this message bus owns the given name. :param name: The name to request. :type name: str :param flags: Name flags that affect the behavior of the name request. :type flags: :class:`NameFlag ` :returns: The reply to the name request. :rtype: :class:`RequestNameReply ` :raises: - :class:`InvalidBusNameError ` - If \ the given bus name is not valid. - :class:`DBusError ` - If the service threw \ an error for the method call or returned an invalid result. - :class:`Exception` - If a connection error occurred. """ future = self._loop.create_future() def reply_handler(reply, err): if err: _future_set_exception(future, err) else: _future_set_result(future, reply) super().request_name(name, flags, reply_handler) return await future async def release_name(self, name: str) -> ReleaseNameReply: """Request that this message bus release the given name. :param name: The name to release. :type name: str :returns: The reply to the release request. :rtype: :class:`ReleaseNameReply ` :raises: - :class:`InvalidBusNameError ` - If \ the given bus name is not valid. - :class:`DBusError ` - If the service threw \ an error for the method call or returned an invalid result. - :class:`Exception` - If a connection error occurred. """ future = self._loop.create_future() def reply_handler(reply, err): if err: _future_set_exception(future, err) else: _future_set_result(future, reply) super().release_name(name, reply_handler) return await future async def call(self, msg: Message) -> Optional[Message]: """Send a method call and wait for a reply from the DBus daemon. :param msg: The method call message to send. :type msg: :class:`Message ` :returns: A message in reply to the message sent. If the message does not expect a reply based on the message flags or type, returns ``None`` after the message is sent. :rtype: :class:`Message ` or :class:`None` if no reply is expected. :raises: - :class:`Exception` - If a connection error occurred. """ if msg.flags & MessageFlag.NO_REPLY_EXPECTED or msg.message_type is not MessageType.METHOD_CALL: await self.send(msg) return None future = self._loop.create_future() def reply_handler(reply, err): if not future.done(): if err: _future_set_exception(future, err) else: _future_set_result(future, reply) self._call(msg, reply_handler) await future return future.result() def send(self, msg: Message): """Asynchronously send a message on the message bus. .. note:: This method may change to a couroutine function in the 1.0 release of the library. :param msg: The message to send. :type msg: :class:`Message ` :returns: A future that resolves when the message is sent or a connection error occurs. :rtype: :class:`Future ` """ if not msg.serial: msg.serial = self.next_serial() future = self._loop.create_future() self._writer.schedule_write(msg, future) return future def get_proxy_object(self, bus_name: str, path: str, introspection: intr.Node) -> ProxyObject: return super().get_proxy_object(bus_name, path, introspection) async def wait_for_disconnect(self): """Wait for the message bus to disconnect. :returns: :class:`None` when the message bus has disconnected. :rtype: :class:`None` :raises: - :class:`Exception` - If connection was terminated unexpectedly or \ an internal error occurred in the library. """ return await self._disconnect_future def _make_method_handler(self, interface, method): if not asyncio.iscoroutinefunction(method.fn): return super()._make_method_handler(interface, method) def handler(msg, send_reply): def done(fut): with send_reply: result = fut.result() body, unix_fds = ServiceInterface._fn_result_to_body( result, method.out_signature_tree) send_reply(Message.new_method_return(msg, method.out_signature, body, unix_fds)) args = ServiceInterface._msg_body_to_args(msg) fut = asyncio.ensure_future(method.fn(interface, *args)) fut.add_done_callback(done) return handler def _message_reader(self): try: while True: if self._unmarshaller.unmarshall(): self._on_message(self._unmarshaller.message) self._unmarshaller = self._create_unmarshaller() else: break except Exception as e: self._finalize(e) async def _auth_readline(self): buf = b'' while buf[-2:] != b'\r\n': buf += await self._loop.sock_recv(self._sock, 2) return buf[:-2].decode() async def _authenticate(self): await self._loop.sock_sendall(self._sock, b'\0') first_line = self._auth._authentication_start(negotiate_unix_fd=self._negotiate_unix_fd) if first_line is not None: if type(first_line) is not str: raise AuthError('authenticator gave response not type str') await self._loop.sock_sendall(self._sock, Authenticator._format_line(first_line)) while True: response = self._auth._receive_line(await self._auth_readline()) if response is not None: await self._loop.sock_sendall(self._sock, Authenticator._format_line(response)) self._stream.flush() if response == 'BEGIN': break def _create_unmarshaller(self): sock = None if self._negotiate_unix_fd: sock = self._sock return Unmarshaller(self._stream, sock) def _finalize(self, err=None): try: self._loop.remove_reader(self._fd) except Exception: logging.warning('could not remove message reader', exc_info=True) try: self._loop.remove_writer(self._fd) except Exception: logging.warning('could not remove message writer', exc_info=True) super()._finalize(err) if self._disconnect_future.done(): return if err and not self._user_disconnect: _future_set_exception(self._disconnect_future, err) else: _future_set_result(self._disconnect_future, None) python-dbus-next-0.2.3/dbus_next/aio/proxy_object.py000066400000000000000000000141631407735772700226200ustar00rootroot00000000000000from ..proxy_object import BaseProxyObject, BaseProxyInterface from ..message_bus import BaseMessageBus from ..message import Message, MessageFlag from ..signature import Variant from ..errors import DBusError from ..constants import ErrorType from .._private.util import replace_idx_with_fds, replace_fds_with_idx from .. import introspection as intr import xml.etree.ElementTree as ET from typing import Union, List class ProxyInterface(BaseProxyInterface): """A class representing a proxy to an interface exported on the bus by another client for the asyncio :class:`MessageBus ` implementation. This class is not meant to be constructed directly by the user. Use :func:`ProxyObject.get_interface() ` on a asyncio proxy object to get a proxy interface. This class exposes methods to call DBus methods, listen to signals, and get and set properties on the interface that are created dynamically based on the introspection data passed to the proxy object that made this proxy interface. A *method call* takes this form: .. code-block:: python3 result = await interface.call_[METHOD](*args) Where ``METHOD`` is the name of the method converted to snake case. DBus methods are exposed as coroutines that take arguments that correpond to the *in args* of the interface method definition and return a ``result`` that corresponds to the *out arg*. If the method has more than one out arg, they are returned within a :class:`list`. To *listen to a signal* use this form: .. code-block:: python3 interface.on_[SIGNAL](callback) To *stop listening to a signal* use this form: .. code-block:: python3 interface.off_[SIGNAL](callback) Where ``SIGNAL`` is the name of the signal converted to snake case. DBus signals are exposed with an event-callback interface. The provided ``callback`` will be called when the signal is emitted with arguments that correspond to the *out args* of the interface signal definition. To *get or set a property* use this form: .. code-block:: python3 value = await interface.get_[PROPERTY]() await interface.set_[PROPERTY](value) Where ``PROPERTY`` is the name of the property converted to snake case. DBus property getters and setters are exposed as coroutines. The ``value`` must correspond to the type of the property in the interface definition. If the service returns an error for a DBus call, a :class:`DBusError ` will be raised with information about the error. """ def _add_method(self, intr_method): async def method_fn(*args, flags=MessageFlag.NONE): input_body, unix_fds = replace_fds_with_idx(intr_method.in_signature, list(args)) msg = await self.bus.call( Message(destination=self.bus_name, path=self.path, interface=self.introspection.name, member=intr_method.name, signature=intr_method.in_signature, body=input_body, flags=flags, unix_fds=unix_fds)) if flags & MessageFlag.NO_REPLY_EXPECTED: return None BaseProxyInterface._check_method_return(msg, intr_method.out_signature) out_len = len(intr_method.out_args) body = replace_idx_with_fds(msg.signature_tree, msg.body, msg.unix_fds) if not out_len: return None elif out_len == 1: return body[0] else: return body method_name = f'call_{BaseProxyInterface._to_snake_case(intr_method.name)}' setattr(self, method_name, method_fn) def _add_property(self, intr_property): async def property_getter(): msg = await self.bus.call( Message(destination=self.bus_name, path=self.path, interface='org.freedesktop.DBus.Properties', member='Get', signature='ss', body=[self.introspection.name, intr_property.name])) BaseProxyInterface._check_method_return(msg, 'v') variant = msg.body[0] if variant.signature != intr_property.signature: raise DBusError(ErrorType.CLIENT_ERROR, f'property returned unexpected signature "{variant.signature}"', msg) return replace_idx_with_fds('v', msg.body, msg.unix_fds)[0].value async def property_setter(val): variant = Variant(intr_property.signature, val) body, unix_fds = replace_fds_with_idx( 'ssv', [self.introspection.name, intr_property.name, variant]) msg = await self.bus.call( Message(destination=self.bus_name, path=self.path, interface='org.freedesktop.DBus.Properties', member='Set', signature='ssv', body=body, unix_fds=unix_fds)) BaseProxyInterface._check_method_return(msg) snake_case = BaseProxyInterface._to_snake_case(intr_property.name) setattr(self, f'get_{snake_case}', property_getter) setattr(self, f'set_{snake_case}', property_setter) class ProxyObject(BaseProxyObject): """The proxy object implementation for the GLib :class:`MessageBus `. For more information, see the :class:`BaseProxyObject `. """ def __init__(self, bus_name: str, path: str, introspection: Union[intr.Node, str, ET.Element], bus: BaseMessageBus): super().__init__(bus_name, path, introspection, bus, ProxyInterface) def get_interface(self, name: str) -> ProxyInterface: return super().get_interface(name) def get_children(self) -> List['ProxyObject']: return super().get_children() python-dbus-next-0.2.3/dbus_next/auth.py000066400000000000000000000070161407735772700203010ustar00rootroot00000000000000from .errors import AuthError import enum import os # The auth interface here is unstable. I would like to eventually open this up # for people to define their own custom authentication protocols, but I'm not # familiar with what's needed for that exactly. To work with any message bus # implementation would require abstracting out all the IO. Async operations # might be challenging because different IO backends have different ways of # doing that. I might just end up giving the raw socket and leaving it all up # to the user, but it would be nice to have a little guidance in the interface # since a lot of it is strongly specified. If you have a need for this, contact # the project maintainer to help stabalize this interface. class _AuthResponse(enum.Enum): OK = 'OK' REJECTED = 'REJECTED' DATA = 'DATA' ERROR = 'ERROR' AGREE_UNIX_FD = 'AGREE_UNIX_FD' @classmethod def parse(klass, line): args = line.split(' ') response = klass(args[0]) return response, args[1:] # UNSTABLE class Authenticator: """The base class for authenticators for :class:`MessageBus ` authentication. In the future, the library may allow extending this class for custom authentication protocols. :seealso: https://dbus.freedesktop.org/doc/dbus-specification.html#auth-protocol """ def _authentication_start(self, negotiate_unix_fd=False): raise NotImplementedError( 'authentication_start() must be implemented in the inheriting class') def _receive_line(self, line): raise NotImplementedError('receive_line() must be implemented in the inheriting class') @staticmethod def _format_line(line): return f'{line}\r\n'.encode() class AuthExternal(Authenticator): """An authenticator class for the external auth protocol for use with the :class:`MessageBus `. :sealso: https://dbus.freedesktop.org/doc/dbus-specification.html#auth-protocol """ def __init__(self): self.negotiate_unix_fd = False self.negotiating_fds = False def _authentication_start(self, negotiate_unix_fd=False) -> str: self.negotiate_unix_fd = negotiate_unix_fd hex_uid = str(os.getuid()).encode().hex() return f'AUTH EXTERNAL {hex_uid}' def _receive_line(self, line: str): response, args = _AuthResponse.parse(line) if response is _AuthResponse.OK: if self.negotiate_unix_fd: self.negotiating_fds = True return "NEGOTIATE_UNIX_FD" else: return "BEGIN" if response is _AuthResponse.AGREE_UNIX_FD: return "BEGIN" raise AuthError(f'authentication failed: {response.value}: {args}') class AuthAnnonymous(Authenticator): """An authenticator class for the annonymous auth protocol for use with the :class:`MessageBus `. :sealso: https://dbus.freedesktop.org/doc/dbus-specification.html#auth-protocol """ def _authentication_start(self, negotiate_unix_fd=False) -> str: if negotiate_unix_fd: raise AuthError( 'annonymous authentication does not support negotiating unix fds right now') return 'AUTH ANONYMOUS' def _receive_line(self, line: str) -> str: response, args = _AuthResponse.parse(line) if response != _AuthResponse.OK: raise AuthError(f'authentication failed: {response.value}: {args}') return 'BEGIN' python-dbus-next-0.2.3/dbus_next/constants.py000066400000000000000000000120641407735772700213530ustar00rootroot00000000000000from enum import Enum, IntFlag class BusType(Enum): """An enum that indicates a type of bus. On most systems, there are normally two different kinds of buses running. """ SESSION = 1 #: A bus for the current graphical user session. SYSTEM = 2 #: A persistent bus for the whole machine. class MessageType(Enum): """An enum that indicates a type of message.""" METHOD_CALL = 1 #: An outgoing method call. METHOD_RETURN = 2 #: A return to a previously sent method call ERROR = 3 #: A return to a method call that has failed SIGNAL = 4 #: A broadcast signal to subscribed connections class MessageFlag(IntFlag): """Flags that affect the behavior of sent and received messages """ NONE = 0 NO_REPLY_EXPECTED = 1 #: The method call does not expect a method return. NO_AUTOSTART = 2 ALLOW_INTERACTIVE_AUTHORIZATION = 4 class NameFlag(IntFlag): """A flag that affects the behavior of a name request. """ NONE = 0 ALLOW_REPLACEMENT = 1 #: If another client requests this name, let them have it. REPLACE_EXISTING = 2 #: If another client owns this name, try to take it. DO_NOT_QUEUE = 4 #: Name requests normally queue and wait for the owner to release the name. Do not enter this queue. class RequestNameReply(Enum): """An enum that describes the result of a name request. """ PRIMARY_OWNER = 1 #: The bus owns the name. IN_QUEUE = 2 #: The bus is in a queue and may receive the name after it is relased by the primary owner. EXISTS = 3 #: The name has an owner and NameFlag.DO_NOT_QUEUE was given. ALREADY_OWNER = 4 #: The bus already owns the name. class ReleaseNameReply(Enum): """An enum that describes the result of a name release request """ RELEASED = 1 NON_EXISTENT = 2 NOT_OWNER = 3 class PropertyAccess(Enum): """An enum that describes whether a DBus property can be gotten or set with the ``org.freedesktop.DBus.Properties`` interface. """ READ = 'read' #: The property is readonly. WRITE = 'write' #: The property is writeonly. READWRITE = 'readwrite' #: The property can be read or written to. def readable(self): """Get whether the property can be read. """ return self == PropertyAccess.READ or self == PropertyAccess.READWRITE def writable(self): """Get whether the property can be written to. """ return self == PropertyAccess.WRITE or self == PropertyAccess.READWRITE class ArgDirection(Enum): """For an introspected argument, indicates whether it is an input parameter or a return value. """ IN = 'in' OUT = 'out' class ErrorType(Enum): """An enum for the type of an error for a message reply. :seealso: http://man7.org/linux/man-pages/man3/sd-bus-errors.3.html """ SERVICE_ERROR = 'com.dubstepdish.dbus.next.ServiceError' #: A custom error to indicate an exported service threw an exception. INTERNAL_ERROR = 'com.dubstepdish.dbus.next.InternalError' #: A custom error to indicate something went wrong with the library. CLIENT_ERROR = 'com.dubstepdish.dbus.next.ClientError' #: A custom error to indicate something went wrong with the client. FAILED = "org.freedesktop.DBus.Error.Failed" NO_MEMORY = "org.freedesktop.DBus.Error.NoMemory" SERVICE_UNKNOWN = "org.freedesktop.DBus.Error.ServiceUnknown" NAME_HAS_NO_OWNER = "org.freedesktop.DBus.Error.NameHasNoOwner" NO_REPLY = "org.freedesktop.DBus.Error.NoReply" IO_ERROR = "org.freedesktop.DBus.Error.IOError" BAD_ADDRESS = "org.freedesktop.DBus.Error.BadAddress" NOT_SUPPORTED = "org.freedesktop.DBus.Error.NotSupported" LIMITS_EXCEEDED = "org.freedesktop.DBus.Error.LimitsExceeded" ACCESS_DENIED = "org.freedesktop.DBus.Error.AccessDenied" AUTH_FAILED = "org.freedesktop.DBus.Error.AuthFailed" NO_SERVER = "org.freedesktop.DBus.Error.NoServer" TIMEOUT = "org.freedesktop.DBus.Error.Timeout" NO_NETWORK = "org.freedesktop.DBus.Error.NoNetwork" ADDRESS_IN_USE = "org.freedesktop.DBus.Error.AddressInUse" DISCONNECTED = "org.freedesktop.DBus.Error.Disconnected" INVALID_ARGS = "org.freedesktop.DBus.Error.InvalidArgs" FILE_NOT_FOUND = "org.freedesktop.DBus.Error.FileNotFound" FILE_EXISTS = "org.freedesktop.DBus.Error.FileExists" UNKNOWN_METHOD = "org.freedesktop.DBus.Error.UnknownMethod" UNKNOWN_OBJECT = "org.freedesktop.DBus.Error.UnknownObject" UNKNOWN_INTERFACE = "org.freedesktop.DBus.Error.UnknownInterface" UNKNOWN_PROPERTY = "org.freedesktop.DBus.Error.UnknownProperty" PROPERTY_READ_ONLY = "org.freedesktop.DBus.Error.PropertyReadOnly" UNIX_PROCESS_ID_UNKNOWN = "org.freedesktop.DBus.Error.UnixProcessIdUnknown" INVALID_SIGNATURE = "org.freedesktop.DBus.Error.InvalidSignature" INCONSISTENT_MESSAGE = "org.freedesktop.DBus.Error.InconsistentMessage" MATCH_RULE_NOT_FOUND = "org.freedesktop.DBus.Error.MatchRuleNotFound" MATCH_RULE_INVALID = "org.freedesktop.DBus.Error.MatchRuleInvalid" INTERACTIVE_AUTHORIZATION_REQUIRED = "org.freedesktop.DBus.Error.InteractiveAuthorizationRequired" python-dbus-next-0.2.3/dbus_next/errors.py000066400000000000000000000033131407735772700206500ustar00rootroot00000000000000class SignatureBodyMismatchError(ValueError): pass class InvalidSignatureError(ValueError): pass class InvalidAddressError(ValueError): pass class AuthError(Exception): pass class InvalidMessageError(ValueError): pass class InvalidIntrospectionError(ValueError): pass class InterfaceNotFoundError(Exception): pass class SignalDisabledError(Exception): pass class InvalidBusNameError(TypeError): def __init__(self, name): super().__init__(f'invalid bus name: {name}') class InvalidObjectPathError(TypeError): def __init__(self, path): super().__init__(f'invalid object path: {path}') class InvalidInterfaceNameError(TypeError): def __init__(self, name): super().__init__(f'invalid interface name: {name}') class InvalidMemberNameError(TypeError): def __init__(self, member): super().__init__(f'invalid member name: {member}') from .message import Message from .validators import assert_interface_name_valid from .constants import ErrorType, MessageType class DBusError(Exception): def __init__(self, type_, text, reply=None): super().__init__(text) if type(type_) is ErrorType: type_ = type_.value assert_interface_name_valid(type_) if reply is not None and type(reply) is not Message: raise TypeError('reply must be of type Message') self.type = type_ self.text = text self.reply = reply @staticmethod def _from_message(msg): assert msg.message_type == MessageType.ERROR return DBusError(msg.error_name, msg.body[0], reply=msg) def _as_message(self, msg): return Message.new_error(msg, self.type, self.text) python-dbus-next-0.2.3/dbus_next/glib/000077500000000000000000000000001407735772700176775ustar00rootroot00000000000000python-dbus-next-0.2.3/dbus_next/glib/__init__.py000066400000000000000000000001321407735772700220040ustar00rootroot00000000000000from .message_bus import MessageBus from .proxy_object import ProxyObject, ProxyInterface python-dbus-next-0.2.3/dbus_next/glib/message_bus.py000066400000000000000000000370741407735772700225610ustar00rootroot00000000000000from .._private.unmarshaller import Unmarshaller from ..constants import BusType from ..message import Message from ..constants import MessageType, MessageFlag, NameFlag, RequestNameReply, ReleaseNameReply from ..message_bus import BaseMessageBus from ..errors import AuthError from .proxy_object import ProxyObject from .. import introspection as intr from ..auth import Authenticator, AuthExternal import io from typing import Callable, Optional # glib is optional _import_error = None try: from gi.repository import GLib _GLibSource = GLib.Source except ImportError as e: _import_error = e class _GLibSource: pass class _MessageSource(_GLibSource): def __init__(self, bus): self.unmarshaller = None self.bus = bus def prepare(self): return (False, -1) def check(self): return False def dispatch(self, callback, user_data): try: while self.bus._stream.readable(): if not self.unmarshaller: self.unmarshaller = Unmarshaller(self.bus._stream) if self.unmarshaller.unmarshall(): callback(self.unmarshaller.message) self.unmarshaller = None else: break except Exception as e: self.bus.disconnect() self.bus._finalize(e) return GLib.SOURCE_REMOVE return GLib.SOURCE_CONTINUE class _MessageWritableSource(_GLibSource): def __init__(self, bus): self.bus = bus self.buf = b'' self.message_stream = None self.chunk_size = 128 def prepare(self): return (False, -1) def check(self): return False def dispatch(self, callback, user_data): try: if self.buf: self.bus._stream.write(self.buf) self.buf = b'' if self.message_stream: while True: self.buf = self.message_stream.read(self.chunk_size) if self.buf == b'': break self.bus._stream.write(self.buf) if len(self.buf) < self.chunk_size: self.buf = b'' break self.buf = b'' self.bus._stream.flush() if not self.bus._buffered_messages: return GLib.SOURCE_REMOVE else: message = self.bus._buffered_messages.pop(0) self.message_stream = io.BytesIO(message._marshall()) return GLib.SOURCE_CONTINUE except BlockingIOError: return GLib.SOURCE_CONTINUE except Exception as e: self.bus._finalize(e) return GLib.SOURCE_REMOVE class _AuthLineSource(_GLibSource): def __init__(self, stream): self.stream = stream self.buf = b'' def prepare(self): return (False, -1) def check(self): return False def dispatch(self, callback, user_data): self.buf += self.stream.read() if self.buf[-2:] == b'\r\n': resp = callback(self.buf.decode()[:-2]) if resp: return GLib.SOURCE_REMOVE return GLib.SOURCE_CONTINUE class MessageBus(BaseMessageBus): """The message bus implementation for use with the GLib main loop. The message bus class is the entry point into all the features of the library. It sets up a connection to the DBus daemon and exposes an interface to send and receive messages and expose services. You must call :func:`connect() ` or :func:`connect_sync() ` before using this message bus. :param bus_type: The type of bus to connect to. Affects the search path for the bus address. :type bus_type: :class:`BusType ` :param bus_address: A specific bus address to connect to. Should not be used under normal circumstances. :param auth: The authenticator to use, defaults to an instance of :class:`AuthExternal `. :type auth: :class:`Authenticator ` :ivar connected: True if this message bus is expected to be able to send and receive messages. :vartype connected: bool :ivar unique_name: The unique name of the message bus connection. It will be :class:`None` until the message bus connects. :vartype unique_name: str """ def __init__(self, bus_address: str = None, bus_type: BusType = BusType.SESSION, auth: Authenticator = None): if _import_error: raise _import_error super().__init__(bus_address, bus_type, ProxyObject) self._main_context = GLib.main_context_default() # buffer messages until connect self._buffered_messages = [] if auth is None: self._auth = AuthExternal() else: self._auth = auth def connect(self, connect_notify: Callable[['MessageBus', Optional[Exception]], None] = None): """Connect this message bus to the DBus daemon. This method or the synchronous version must be called before the message bus can be used. :param connect_notify: A callback that will be called with this message bus. May return an :class:`Exception` on connection errors or :class:`AuthError ` on authorization errors. :type callback: :class:`Callable` """ def authenticate_notify(exc): if exc is not None: if connect_notify is not None: connect_notify(None, exc) return self.message_source = _MessageSource(self) self.message_source.set_callback(self._on_message) self.message_source.attach(self._main_context) self.writable_source = None self.message_source.add_unix_fd(self._fd, GLib.IO_IN) def on_hello(reply, err): if err: if connect_notify: connect_notify(reply, err) return self.unique_name = reply.body[0] for m in self._buffered_messages: self.send(m) if connect_notify: connect_notify(self, err) hello_msg = Message(destination='org.freedesktop.DBus', path='/org/freedesktop/DBus', interface='org.freedesktop.DBus', member='Hello', serial=self.next_serial()) self._method_return_handlers[hello_msg.serial] = on_hello self._stream.write(hello_msg._marshall()) self._stream.flush() self._authenticate(authenticate_notify) def connect_sync(self) -> 'MessageBus': """Connect this message bus to the DBus daemon. This method or the asynchronous version must be called before the message bus can be used. :returns: This message bus for convenience. :rtype: :class:`MessageBus ` :raises: - :class:`AuthError ` - If authorization to \ the DBus daemon failed. - :class:`Exception` - If there was a connection error. """ main = GLib.MainLoop() connection_error = None def connect_notify(bus, err): nonlocal connection_error connection_error = err main.quit() self.connect(connect_notify) main.run() if connection_error: raise connection_error return self def call(self, msg: Message, reply_notify: Callable[[Optional[Message], Optional[Exception]], None] = None): """Send a method call and asynchronously wait for a reply from the DBus daemon. :param msg: The method call message to send. :type msg: :class:`Message ` :param reply_notify: A callback that will be called with the reply to this message. May return an :class:`Exception` on connection errors. :type reply_notify: Callable """ self._call(msg, reply_notify) def call_sync(self, msg: Message) -> Optional[Message]: """Send a method call and synchronously wait for a reply from the DBus daemon. :param msg: The method call message to send. :type msg: :class:`Message ` :returns: A message in reply to the message sent. If the message does not expect a reply based on the message flags or type, returns ``None`` immediately. :rtype: :class:`Message ` :raises: - :class:`DBusError ` - If the service threw \ an error for the method call or returned an invalid result. - :class:`Exception` - If a connection error occurred. """ if msg.flags & MessageFlag.NO_REPLY_EXPECTED or msg.message_type is not MessageType.METHOD_CALL: self.send(msg) return None if not msg.serial: msg.serial = self.next_serial() main = GLib.MainLoop() handler_reply = None connection_error = None def reply_handler(reply, err): nonlocal handler_reply nonlocal connection_error handler_reply = reply connection_error = err main.quit() self._method_return_handlers[msg.serial] = reply_handler self.send(msg) main.run() if connection_error: raise connection_error return handler_reply def introspect_sync(self, bus_name: str, path: str) -> intr.Node: """Get introspection data for the node at the given path from the given bus name. Calls the standard ``org.freedesktop.DBus.Introspectable.Introspect`` on the bus for the path. :param bus_name: The name to introspect. :type bus_name: str :param path: The path to introspect. :type path: str :returns: The introspection data for the name at the path. :rtype: :class:`Node ` :raises: - :class:`InvalidObjectPathError ` \ - If the given object path is not valid. - :class:`InvalidBusNameError ` - If \ the given bus name is not valid. - :class:`DBusError ` - If the service threw \ an error for the method call or returned an invalid result. - :class:`Exception` - If a connection error occurred. """ main = GLib.MainLoop() request_result = None request_error = None def reply_notify(result, err): nonlocal request_result nonlocal request_error request_result = result request_error = err main.quit() super().introspect(bus_name, path, reply_notify) main.run() if request_error: raise request_error return request_result def request_name_sync(self, name: str, flags: NameFlag = NameFlag.NONE) -> RequestNameReply: """Request that this message bus owns the given name. :param name: The name to request. :type name: str :param flags: Name flags that affect the behavior of the name request. :type flags: :class:`NameFlag ` :returns: The reply to the name request. :rtype: :class:`RequestNameReply ` :raises: - :class:`InvalidBusNameError ` - If \ the given bus name is not valid. - :class:`DBusError ` - If the service threw \ an error for the method call or returned an invalid result. - :class:`Exception` - If a connection error occurred. """ main = GLib.MainLoop() request_result = None request_error = None def reply_notify(result, err): nonlocal request_result nonlocal request_error request_result = result request_error = err main.quit() super().request_name(name, flags, reply_notify) main.run() if request_error: raise request_error return request_result def release_name_sync(self, name: str) -> ReleaseNameReply: """Request that this message bus release the given name. :param name: The name to release. :type name: str :returns: The reply to the release request. :rtype: :class:`ReleaseNameReply ` :raises: - :class:`InvalidBusNameError ` - If \ the given bus name is not valid. - :class:`DBusError ` - If the service threw \ an error for the method call or returned an invalid result. - :class:`Exception` - If a connection error occurred. """ main = GLib.MainLoop() release_result = None release_error = None def reply_notify(result, err): nonlocal release_result nonlocal release_error release_result = result release_error = err main.quit() super().release_name(name, reply_notify) main.run() if release_error: raise release_error return release_result def send(self, msg: Message): if not msg.serial: msg.serial = self.next_serial() self._buffered_messages.append(msg) if self.unique_name: self._schedule_write() def get_proxy_object(self, bus_name: str, path: str, introspection: intr.Node) -> ProxyObject: return super().get_proxy_object(bus_name, path, introspection) def _schedule_write(self): if self.writable_source is None or self.writable_source.is_destroyed(): self.writable_source = _MessageWritableSource(self) self.writable_source.attach(self._main_context) self.writable_source.add_unix_fd(self._fd, GLib.IO_OUT) def _authenticate(self, authenticate_notify): self._stream.write(b'\0') first_line = self._auth._authentication_start() if first_line is not None: if type(first_line) is not str: raise AuthError('authenticator gave response not type str') self._stream.write(f'{first_line}\r\n'.encode()) self._stream.flush() def line_notify(line): try: resp = self._auth._receive_line(line) self._stream.write(Authenticator._format_line(resp)) self._stream.flush() if resp == 'BEGIN': self._readline_source = None authenticate_notify(None) return True except Exception as e: authenticate_notify(e) return True readline_source = _AuthLineSource(self._stream) readline_source.set_callback(line_notify) readline_source.add_unix_fd(self._fd, GLib.IO_IN) readline_source.attach(self._main_context) # make sure it doesnt get cleaned up self._readline_source = readline_source python-dbus-next-0.2.3/dbus_next/glib/proxy_object.py000066400000000000000000000233161407735772700227650ustar00rootroot00000000000000from ..proxy_object import BaseProxyObject, BaseProxyInterface from ..message_bus import BaseMessageBus from ..message import Message from ..errors import DBusError from ..signature import Variant from ..constants import ErrorType from .. import introspection as intr import xml.etree.ElementTree as ET from typing import Union, List # glib is optional try: from gi.repository import GLib except ImportError: pass class ProxyInterface(BaseProxyInterface): """A class representing a proxy to an interface exported on the bus by another client for the GLib :class:`MessageBus ` implementation. This class is not meant to be constructed directly by the user. Use :func:`ProxyObject.get_interface() ` on a GLib proxy object to get a proxy interface. This class exposes methods to call DBus methods, listen to signals, and get and set properties on the interface that are created dynamically based on the introspection data passed to the proxy object that made this proxy interface. A *method call* takes this form: .. code-block:: python3 def callback(error: Exception, result: list(Any)): pass interface.call_[METHOD](*args, callback) result = interface.call_[METHOD]_sync(*args) Where ``METHOD`` is the name of the method converted to snake case. To call a method, provide ``*args`` that correspond to the *in args* of the introspection method definition. To *asynchronously* call a method, provide a callback that takes an error as the first argument and a list as the second argument. If the call completed successfully, ``error`` will be :class:`None`. If the service returns an error, it will be a :class:`DBusError ` with information about the error returned from the bus. The result will be a list of values that correspond to the *out args* of the introspection method definition. To *synchronously* call a method, use the ``call_[METHOD]_sync()`` form. The ``result`` corresponds to the *out arg* of the introspection method definition. If the method has more than one otu arg, they are returned within a :class:`list`. To *listen to a signal* use this form: .. code-block:: python3 interface.on_[SIGNAL](callback) To *stop listening to a signal* use this form: .. code-block:: python3 interface.off_[SIGNAL](callback) Where ``SIGNAL`` is the name of the signal converted to snake case. DBus signals are exposed with an event-callback interface. The provided ``callback`` will be called when the signal is emitted with arguments that correspond to the *out args* of the interface signal definition. To *get or set a property* use this form: .. code-block:: python3 def get_callback(error: Exception, value: Any): pass def set_callback(error: Exception) pass interface.get_[PROPERTY](get_callback) value: Any = interface.get_[PROPERTY]_sync() interface.set_[PROPERTY](set_callback) interface.set_[PROPERTY]_sync(value) Where ``PROPERTY`` is the name of the property converted to snake case. The ``value`` must correspond to the type of the property in the interface definition. To asynchronously get or set a property, provide a callback that takes an :class:`Exception` as the first argument. If the call completed successfully, ``error`` will be :class:`None`. If the service returns an error, it will be a :class:`DBusError ` with information about the error returned from the bus. If the service returns an error for a synchronous DBus call, a :class:`DBusError ` will be raised with information about the error. """ def _add_method(self, intr_method): in_len = len(intr_method.in_args) out_len = len(intr_method.out_args) def method_fn(*args): if len(args) != in_len + 1: raise TypeError( f'method {intr_method.name} expects {in_len} arguments and a callback (got {len(args)} args)' ) args = list(args) # TODO type check: this callback takes two parameters # (MessageBus.check_callback(cb)) callback = args.pop() def call_notify(msg, err): if err: callback([], err) return try: BaseProxyInterface._check_method_return(msg, intr_method.out_signature) except DBusError as e: err = e callback(msg.body, err) self.bus.call( Message(destination=self.bus_name, path=self.path, interface=self.introspection.name, member=intr_method.name, signature=intr_method.in_signature, body=list(args)), call_notify) def method_fn_sync(*args): main = GLib.MainLoop() call_error = None call_body = None def callback(body, err): nonlocal call_error nonlocal call_body call_error = err call_body = body main.quit() method_fn(*args, callback) main.run() if call_error: raise call_error if not out_len: return None elif out_len == 1: return call_body[0] else: return call_body method_name = f'call_{BaseProxyInterface._to_snake_case(intr_method.name)}' method_name_sync = f'{method_name}_sync' setattr(self, method_name, method_fn) setattr(self, method_name_sync, method_fn_sync) def _add_property(self, intr_property): def property_getter(callback): def call_notify(msg, err): if err: callback(None, err) return try: BaseProxyInterface._check_method_return(msg) except Exception as e: callback(None, e) return variant = msg.body[0] if variant.signature != intr_property.signature: err = DBusError(ErrorType.CLIENT_ERROR, 'property returned unexpected signature "{variant.signature}"', msg) callback(None, err) return callback(variant.value, None) self.bus.call( Message(destination=self.bus_name, path=self.path, interface='org.freedesktop.DBus.Properties', member='Get', signature='ss', body=[self.introspection.name, intr_property.name]), call_notify) def property_getter_sync(): property_value = None reply_error = None main = GLib.MainLoop() def callback(value, err): nonlocal property_value nonlocal reply_error property_value = value reply_error = err main.quit() property_getter(callback) main.run() if reply_error: raise reply_error return property_value def property_setter(value, callback): def call_notify(msg, err): if err: callback(None, err) return try: BaseProxyInterface._check_method_return(msg) except Exception as e: callback(None, e) return return callback(None, None) variant = Variant(intr_property.signature, value) self.bus.call( Message(destination=self.bus_name, path=self.path, interface='org.freedesktop.DBus.Properties', member='Set', signature='ssv', body=[self.introspection.name, intr_property.name, variant]), call_notify) def property_setter_sync(val): reply_error = None main = GLib.MainLoop() def callback(value, err): nonlocal reply_error reply_error = err main.quit() property_setter(val, callback) main.run() if reply_error: raise reply_error return None snake_case = super()._to_snake_case(intr_property.name) setattr(self, f'get_{snake_case}', property_getter) setattr(self, f'get_{snake_case}_sync', property_getter_sync) setattr(self, f'set_{snake_case}', property_setter) setattr(self, f'set_{snake_case}_sync', property_setter_sync) class ProxyObject(BaseProxyObject): """The proxy object implementation for the asyncio :class:`MessageBus `. For more information, see the :class:`BaseProxyObject `. """ def __init__(self, bus_name: str, path: str, introspection: Union[intr.Node, str, ET.Element], bus: BaseMessageBus): super().__init__(bus_name, path, introspection, bus, ProxyInterface) def get_interface(self, name: str) -> ProxyInterface: return super().get_interface(name) def get_children(self) -> List['ProxyObject']: return super().get_children() python-dbus-next-0.2.3/dbus_next/introspection.py000066400000000000000000000541361407735772700222450ustar00rootroot00000000000000from .constants import PropertyAccess, ArgDirection from .signature import SignatureTree, SignatureType from .validators import assert_member_name_valid, assert_interface_name_valid from .errors import InvalidIntrospectionError from typing import List, Union import xml.etree.ElementTree as ET # https://dbus.freedesktop.org/doc/dbus-specification.html#introspection-format # TODO annotations class Arg: """A class that represents an input or output argument to a signal or a method. :ivar name: The name of this arg. :vartype name: str :ivar direction: Whether this is an input or an output argument. :vartype direction: :class:`ArgDirection ` :ivar type: The parsed signature type of this argument. :vartype type: :class:`SignatureType ` :ivar signature: The signature string of this argument. :vartype signature: str :raises: - :class:`InvalidMemberNameError ` - If the name of the arg is not valid. - :class:`InvalidSignatureError ` - If the signature is not valid. - :class:`InvalidIntrospectionError ` - If the signature is not a single complete type. """ def __init__(self, signature: Union[SignatureType, str], direction: List[ArgDirection] = None, name: str = None): if name is not None: assert_member_name_valid(name) type_ = None if type(signature) is SignatureType: type_ = signature signature = signature.signature else: tree = SignatureTree._get(signature) if len(tree.types) != 1: raise InvalidIntrospectionError( f'an argument must have a single complete type. (has {len(tree.types)} types)') type_ = tree.types[0] self.type = type_ self.signature = signature self.name = name self.direction = direction def from_xml(element: ET.Element, direction: ArgDirection) -> 'Arg': """Convert a :class:`xml.etree.ElementTree.Element` into a :class:`Arg`. The element must be valid DBus introspection XML for an ``arg``. :param element: The parsed XML element. :type element: :class:`xml.etree.ElementTree.Element` :param direction: The direction of this arg. Must be specified because it can default to different values depending on if it's in a method or signal. :type direction: :class:`ArgDirection ` :raises: - :class:`InvalidIntrospectionError ` - If the XML tree is not valid introspection data. """ name = element.attrib.get('name') signature = element.attrib.get('type') if not signature: raise InvalidIntrospectionError('a method argument must have a "type" attribute') return Arg(signature, direction, name) def to_xml(self) -> ET.Element: """Convert this :class:`Arg` into an :class:`xml.etree.ElementTree.Element`. """ element = ET.Element('arg') if self.name: element.set('name', self.name) if self.direction: element.set('direction', self.direction.value) element.set('type', self.signature) return element class Signal: """A class that represents a signal exposed on an interface. :ivar name: The name of this signal :vartype name: str :ivar args: A list of output arguments for this signal. :vartype args: list(Arg) :ivar signature: The collected signature of the output arguments. :vartype signature: str :raises: - :class:`InvalidMemberNameError ` - If the name of the signal is not a valid member name. """ def __init__(self, name: str, args: List[Arg] = None): if name is not None: assert_member_name_valid(name) self.name = name self.args = args or [] self.signature = ''.join(arg.signature for arg in self.args) def from_xml(element): """Convert an :class:`xml.etree.ElementTree.Element` to a :class:`Signal`. The element must be valid DBus introspection XML for a ``signal``. :param element: The parsed XML element. :type element: :class:`xml.etree.ElementTree.Element` :param is_root: Whether this is the root node :type is_root: bool :raises: - :class:`InvalidIntrospectionError ` - If the XML tree is not valid introspection data. """ name = element.attrib.get('name') if not name: raise InvalidIntrospectionError('signals must have a "name" attribute') args = [] for child in element: if child.tag == 'arg': args.append(Arg.from_xml(child, ArgDirection.OUT)) signal = Signal(name, args) return signal def to_xml(self) -> ET.Element: """Convert this :class:`Signal` into an :class:`xml.etree.ElementTree.Element`. """ element = ET.Element('signal') element.set('name', self.name) for arg in self.args: element.append(arg.to_xml()) return element class Method: """A class that represents a method exposed on an :class:`Interface`. :ivar name: The name of this method. :vartype name: str :ivar in_args: A list of input arguments to this method. :vartype in_args: list(Arg) :ivar out_args: A list of output arguments to this method. :vartype out_args: list(Arg) :ivar in_signature: The collected signature string of the input arguments. :vartype in_signature: str :ivar out_signature: The collected signature string of the output arguments. :vartype out_signature: str :raises: - :class:`InvalidMemberNameError ` - If the name of this method is not valid. """ def __init__(self, name: str, in_args: List[Arg] = [], out_args: List[Arg] = []): assert_member_name_valid(name) self.name = name self.in_args = in_args self.out_args = out_args self.in_signature = ''.join(arg.signature for arg in in_args) self.out_signature = ''.join(arg.signature for arg in out_args) def from_xml(element: ET.Element) -> 'Method': """Convert an :class:`xml.etree.ElementTree.Element` to a :class:`Method`. The element must be valid DBus introspection XML for a ``method``. :param element: The parsed XML element. :type element: :class:`xml.etree.ElementTree.Element` :param is_root: Whether this is the root node :type is_root: bool :raises: - :class:`InvalidIntrospectionError ` - If the XML tree is not valid introspection data. """ name = element.attrib.get('name') if not name: raise InvalidIntrospectionError('interfaces must have a "name" attribute') in_args = [] out_args = [] for child in element: if child.tag == 'arg': direction = ArgDirection(child.attrib.get('direction', 'in')) arg = Arg.from_xml(child, direction) if direction == ArgDirection.IN: in_args.append(arg) elif direction == ArgDirection.OUT: out_args.append(arg) return Method(name, in_args, out_args) def to_xml(self) -> ET.Element: """Convert this :class:`Method` into an :class:`xml.etree.ElementTree.Element`. """ element = ET.Element('method') element.set('name', self.name) for arg in self.in_args: element.append(arg.to_xml()) for arg in self.out_args: element.append(arg.to_xml()) return element class Property: """A class that represents a DBus property exposed on an :class:`Interface`. :ivar name: The name of this property. :vartype name: str :ivar signature: The signature string for this property. Must be a single complete type. :vartype signature: str :ivar access: Whether this property is readable and writable. :vartype access: :class:`PropertyAccess ` :ivar type: The parsed type of this property. :vartype type: :class:`SignatureType ` :raises: - :class:`InvalidIntrospectionError ` - If the property is not a single complete type. - :class `InvalidSignatureError ` - If the given signature is not valid. - :class: `InvalidMemberNameError ` - If the member name is not valid. """ def __init__(self, name: str, signature: str, access: PropertyAccess = PropertyAccess.READWRITE): assert_member_name_valid(name) tree = SignatureTree._get(signature) if len(tree.types) != 1: raise InvalidIntrospectionError( f'properties must have a single complete type. (has {len(tree.types)} types)') self.name = name self.signature = signature self.access = access self.type = tree.types[0] def from_xml(element): """Convert an :class:`xml.etree.ElementTree.Element` to a :class:`Property`. The element must be valid DBus introspection XML for a ``property``. :param element: The parsed XML element. :type element: :class:`xml.etree.ElementTree.Element` :raises: - :class:`InvalidIntrospectionError ` - If the XML tree is not valid introspection data. """ name = element.attrib.get('name') signature = element.attrib.get('type') access = PropertyAccess(element.attrib.get('access', 'readwrite')) if not name: raise InvalidIntrospectionError('properties must have a "name" attribute') if not signature: raise InvalidIntrospectionError('properties must have a "type" attribute') return Property(name, signature, access) def to_xml(self) -> ET.Element: """Convert this :class:`Property` into an :class:`xml.etree.ElementTree.Element`. """ element = ET.Element('property') element.set('name', self.name) element.set('type', self.signature) element.set('access', self.access.value) return element class Interface: """A class that represents a DBus interface exported on on object path. Contains information about the methods, signals, and properties exposed on this interface. :ivar name: The name of this interface. :vartype name: str :ivar methods: A list of methods exposed on this interface. :vartype methods: list(:class:`Method`) :ivar signals: A list of signals exposed on this interface. :vartype signals: list(:class:`Signal`) :ivar properties: A list of properties exposed on this interface. :vartype properties: list(:class:`Property`) :raises: - :class:`InvalidInterfaceNameError ` - If the name is not a valid interface name. """ def __init__(self, name: str, methods: List[Method] = None, signals: List[Signal] = None, properties: List[Property] = None): assert_interface_name_valid(name) self.name = name self.methods = methods if methods is not None else [] self.signals = signals if signals is not None else [] self.properties = properties if properties is not None else [] @staticmethod def from_xml(element: ET.Element) -> 'Interface': """Convert a :class:`xml.etree.ElementTree.Element` into a :class:`Interface`. The element must be valid DBus introspection XML for an ``interface``. :param element: The parsed XML element. :type element: :class:`xml.etree.ElementTree.Element` :raises: - :class:`InvalidIntrospectionError ` - If the XML tree is not valid introspection data. """ name = element.attrib.get('name') if not name: raise InvalidIntrospectionError('interfaces must have a "name" attribute') interface = Interface(name) for child in element: if child.tag == 'method': interface.methods.append(Method.from_xml(child)) elif child.tag == 'signal': interface.signals.append(Signal.from_xml(child)) elif child.tag == 'property': interface.properties.append(Property.from_xml(child)) return interface def to_xml(self) -> ET.Element: """Convert this :class:`Interface` into an :class:`xml.etree.ElementTree.Element`. """ element = ET.Element('interface') element.set('name', self.name) for method in self.methods: element.append(method.to_xml()) for signal in self.signals: element.append(signal.to_xml()) for prop in self.properties: element.append(prop.to_xml()) return element class Node: """A class that represents a node in an object path in introspection data. A node contains information about interfaces exported on this path and child nodes. A node can be converted to and from introspection XML exposed through the ``org.freedesktop.DBus.Introspectable`` standard DBus interface. This class is an essential building block for a high-level DBus interface. This is the underlying data structure for the :class:`ProxyObject `. A :class:`ServiceInterface ` definition is converted to this class to expose XML on the introspectable interface. :ivar interfaces: A list of interfaces exposed on this node. :vartype interfaces: list(:class:`Interface `) :ivar nodes: A list of child nodes. :vartype nodes: list(:class:`Node`) :ivar name: The object path of this node. :vartype name: str :ivar is_root: Whether this is the root node. False if it is a child node. :vartype is_root: bool :raises: - :class:`InvalidIntrospectionError ` - If the name is not a valid node name. """ def __init__(self, name: str = None, interfaces: List[Interface] = None, is_root: bool = True): if not is_root and not name: raise InvalidIntrospectionError('child nodes must have a "name" attribute') self.interfaces = interfaces if interfaces is not None else [] self.nodes = [] self.name = name self.is_root = is_root @staticmethod def from_xml(element: ET.Element, is_root: bool = False): """Convert an :class:`xml.etree.ElementTree.Element` to a :class:`Node`. The element must be valid DBus introspection XML for a ``node``. :param element: The parsed XML element. :type element: :class:`xml.etree.ElementTree.Element` :param is_root: Whether this is the root node :type is_root: bool :raises: - :class:`InvalidIntrospectionError ` - If the XML tree is not valid introspection data. """ node = Node(element.attrib.get('name'), is_root=is_root) for child in element: if child.tag == 'interface': node.interfaces.append(Interface.from_xml(child)) elif child.tag == 'node': node.nodes.append(Node.from_xml(child)) return node @staticmethod def parse(data: str) -> 'Node': """Parse XML data as a string into a :class:`Node`. The string must be valid DBus introspection XML. :param data: The XMl string. :type data: str :raises: - :class:`InvalidIntrospectionError ` - If the string is not valid introspection data. """ element = ET.fromstring(data) if element.tag != 'node': raise InvalidIntrospectionError( 'introspection data must have a "node" for the root element') return Node.from_xml(element, is_root=True) def to_xml(self) -> ET.Element: """Convert this :class:`Node` into an :class:`xml.etree.ElementTree.Element`. """ element = ET.Element('node') if self.name: element.set('name', self.name) for interface in self.interfaces: element.append(interface.to_xml()) for node in self.nodes: element.append(node.to_xml()) return element def tostring(self) -> str: """Convert this :class:`Node` into a DBus introspection XML string. """ header = '\n' def indent(elem, level=0): i = "\n" + level * " " if len(elem): if not elem.text or not elem.text.strip(): elem.text = i + " " if not elem.tail or not elem.tail.strip(): elem.tail = i for elem in elem: indent(elem, level + 1) if not elem.tail or not elem.tail.strip(): elem.tail = i else: if level and (not elem.tail or not elem.tail.strip()): elem.tail = i xml = self.to_xml() indent(xml) return header + ET.tostring(xml, encoding='unicode').rstrip() @staticmethod def default(name: str = None) -> 'Node': """Create a :class:`Node` with the default interfaces supported by this library. The default interfaces include: * ``org.freedesktop.DBus.Introspectable`` * ``org.freedesktop.DBus.Peer`` * ``org.freedesktop.DBus.Properties`` * ``org.freedesktop.DBus.ObjectManager`` """ return Node(name, is_root=True, interfaces=[ Interface('org.freedesktop.DBus.Introspectable', methods=[ Method('Introspect', out_args=[Arg('s', ArgDirection.OUT, 'data')]) ]), Interface('org.freedesktop.DBus.Peer', methods=[ Method('GetMachineId', out_args=[Arg('s', ArgDirection.OUT, 'machine_uuid')]), Method('Ping') ]), Interface('org.freedesktop.DBus.Properties', methods=[ Method('Get', in_args=[ Arg('s', ArgDirection.IN, 'interface_name'), Arg('s', ArgDirection.IN, 'property_name') ], out_args=[Arg('v', ArgDirection.OUT, 'value')]), Method('Set', in_args=[ Arg('s', ArgDirection.IN, 'interface_name'), Arg('s', ArgDirection.IN, 'property_name'), Arg('v', ArgDirection.IN, 'value') ]), Method('GetAll', in_args=[Arg('s', ArgDirection.IN, 'interface_name')], out_args=[Arg('a{sv}', ArgDirection.OUT, 'props')]) ], signals=[ Signal('PropertiesChanged', args=[ Arg('s', ArgDirection.OUT, 'interface_name'), Arg('a{sv}', ArgDirection.OUT, 'changed_properties'), Arg('as', ArgDirection.OUT, 'invalidated_properties') ]) ]), Interface('org.freedesktop.DBus.ObjectManager', methods=[ Method('GetManagedObjects', out_args=[ Arg('a{oa{sa{sv}}}', ArgDirection.OUT, 'objpath_interfaces_and_properties') ]), ], signals=[ Signal('InterfacesAdded', args=[ Arg('o', ArgDirection.OUT, 'object_path'), Arg('a{sa{sv}}', ArgDirection.OUT, 'interfaces_and_properties'), ]), Signal('InterfacesRemoved', args=[ Arg('o', ArgDirection.OUT, 'object_path'), Arg('as', ArgDirection.OUT, 'interfaces'), ]) ]), ]) python-dbus-next-0.2.3/dbus_next/message.py000066400000000000000000000253261407735772700207700ustar00rootroot00000000000000from ._private.marshaller import Marshaller from .constants import MessageType, MessageFlag, ErrorType from ._private.constants import PROTOCOL_VERSION, HeaderField, LITTLE_ENDIAN from .validators import assert_bus_name_valid, assert_member_name_valid, assert_object_path_valid, assert_interface_name_valid from .errors import InvalidMessageError from .signature import SignatureTree, Variant from typing import List, Any class Message: """A class for sending and receiving messages through the :class:`MessageBus ` with the low-level api. A ``Message`` can be constructed by the user to send over the message bus. When messages are received, such as from method calls or signal emissions, they will use this class as well. :ivar destination: The address of the client for which this message is intended. :vartype destination: str :ivar path: The intended object path exported on the destination bus. :vartype path: str :ivar interface: The intended interface on the object path. :vartype interface: str :ivar member: The intended member on the interface. :vartype member: str :ivar message_type: The type of this message. A method call, signal, method return, or error. :vartype message_type: :class:`MessageType` :ivar flags: Flags that affect the behavior of this message. :vartype flags: :class:`MessageFlag` :ivar error_name: If this message is an error, the name of this error. Must be a valid interface name. :vartype error_name: str :ivar reply_serial: If this is a return type, the serial this message is in reply to. :vartype reply_serial: int :ivar sender: The address of the sender of this message. Will be a unique name. :vartype sender: str :ivar unix_fds: A list of unix fds that were sent in the header of this message. :vartype unix_fds: list(int) :ivar signature: The signature of the body of this message. :vartype signature: str :ivar signature_tree: The signature parsed as a signature tree. :vartype signature_tree: :class:`SignatureTree` :ivar body: The body of this message. Must match the signature. :vartype body: list(Any) :ivar serial: The serial of the message. Will be automatically set during message sending if not present. Use the ``new_serial()`` method of the bus to generate a serial. :vartype serial: int :raises: - :class:`InvalidMessageError` - If the message is malformed or missing fields for the message type. - :class:`InvalidSignatureError` - If the given signature is not valid. - :class:`InvalidObjectPathError` - If ``path`` is not a valid object path. - :class:`InvalidBusNameError` - If ``destination`` is not a valid bus name. - :class:`InvalidMemberNameError` - If ``member`` is not a valid member name. - :class:`InvalidInterfaceNameError` - If ``error_name`` or ``interface`` is not a valid interface name. """ def __init__(self, destination: str = None, path: str = None, interface: str = None, member: str = None, message_type: MessageType = MessageType.METHOD_CALL, flags: MessageFlag = MessageFlag.NONE, error_name: str = None, reply_serial: int = None, sender: str = None, unix_fds: List[int] = [], signature: str = '', body: List[Any] = [], serial: int = 0): self.destination = destination self.path = path self.interface = interface self.member = member self.message_type = message_type self.flags = flags if type(flags) is MessageFlag else MessageFlag(bytes([flags])) self.error_name = error_name if type(error_name) is not ErrorType else error_name.value self.reply_serial = reply_serial self.sender = sender self.unix_fds = unix_fds self.signature = signature.signature if type(signature) is SignatureTree else signature self.signature_tree = signature if type(signature) is SignatureTree else SignatureTree._get( signature) self.body = body self.serial = serial if self.destination is not None: assert_bus_name_valid(self.destination) if self.interface is not None: assert_interface_name_valid(self.interface) if self.path is not None: assert_object_path_valid(self.path) if self.member is not None: assert_member_name_valid(self.member) if self.error_name is not None: assert_interface_name_valid(self.error_name) def require_fields(*fields): for field in fields: if not getattr(self, field): raise InvalidMessageError(f'missing required field: {field}') if self.message_type == MessageType.METHOD_CALL: require_fields('path', 'member') elif self.message_type == MessageType.SIGNAL: require_fields('path', 'member', 'interface') elif self.message_type == MessageType.ERROR: require_fields('error_name', 'reply_serial') elif self.message_type == MessageType.METHOD_RETURN: require_fields('reply_serial') else: raise InvalidMessageError(f'got unknown message type: {self.message_type}') @staticmethod def new_error(msg: 'Message', error_name: str, error_text: str) -> 'Message': """A convenience constructor to create an error message in reply to the given message. :param msg: The message this error is in reply to. :type msg: :class:`Message` :param error_name: The name of this error. Must be a valid interface name. :type error_name: str :param error_text: Human-readable text for the error. :returns: The error message. :rtype: :class:`Message` :raises: - :class:`InvalidInterfaceNameError` - If the error_name is not a valid interface name. """ return Message(message_type=MessageType.ERROR, reply_serial=msg.serial, destination=msg.sender, error_name=error_name, signature='s', body=[error_text]) @staticmethod def new_method_return(msg: 'Message', signature: str = '', body: List[Any] = [], unix_fds: List[int] = []) -> 'Message': """A convenience constructor to create a method return to the given method call message. :param msg: The method call message this is a reply to. :type msg: :class:`Message` :param signature: The signature for the message body. :type signature: str :param body: The body of this message. Must match the signature. :type body: list(Any) :param unix_fds: List integer file descriptors to send with this message. :type body: list(int) :returns: The method return message :rtype: :class:`Message` :raises: - :class:`InvalidSignatureError` - If the signature is not a valid signature. """ return Message(message_type=MessageType.METHOD_RETURN, reply_serial=msg.serial, destination=msg.sender, signature=signature, body=body, unix_fds=unix_fds) @staticmethod def new_signal(path: str, interface: str, member: str, signature: str = '', body: List[Any] = None, unix_fds: List[int] = None) -> 'Message': """A convenience constructor to create a new signal message. :param path: The path of this signal. :type path: str :param interface: The interface of this signal. :type interface: str :param member: The member name of this signal. :type member: str :param signature: The signature of the signal body. :type signature: str :param body: The body of this signal message. :type body: list(Any) :param unix_fds: List integer file descriptors to send with this message. :type body: list(int) :returns: The signal message. :rtype: :class:`Message` :raises: - :class:`InvalidSignatureError` - If the signature is not a valid signature. - :class:`InvalidObjectPathError` - If ``path`` is not a valid object path. - :class:`InvalidInterfaceNameError` - If ``interface`` is not a valid interface name. - :class:`InvalidMemberNameError` - If ``member`` is not a valid member name. """ body = body if body else [] return Message(message_type=MessageType.SIGNAL, interface=interface, path=path, member=member, signature=signature, body=body, unix_fds=unix_fds) def _matches(self, **kwargs): for attr, val in kwargs.items(): if getattr(self, attr) != val: return False return True def _marshall(self, negotiate_unix_fd=False): # TODO maximum message size is 134217728 (128 MiB) body_block = Marshaller(self.signature, self.body) body_block.marshall() fields = [] if self.path: fields.append([HeaderField.PATH.value, Variant('o', self.path)]) if self.interface: fields.append([HeaderField.INTERFACE.value, Variant('s', self.interface)]) if self.member: fields.append([HeaderField.MEMBER.value, Variant('s', self.member)]) if self.error_name: fields.append([HeaderField.ERROR_NAME.value, Variant('s', self.error_name)]) if self.reply_serial: fields.append([HeaderField.REPLY_SERIAL.value, Variant('u', self.reply_serial)]) if self.destination: fields.append([HeaderField.DESTINATION.value, Variant('s', self.destination)]) if self.signature: fields.append([HeaderField.SIGNATURE.value, Variant('g', self.signature)]) if self.unix_fds and negotiate_unix_fd: fields.append([HeaderField.UNIX_FDS.value, Variant('u', len(self.unix_fds))]) header_body = [ LITTLE_ENDIAN, self.message_type.value, self.flags.value, PROTOCOL_VERSION, len(body_block.buffer), self.serial, fields ] header_block = Marshaller('yyyyuua(yv)', header_body) header_block.marshall() header_block.align(8) return header_block.buffer + body_block.buffer python-dbus-next-0.2.3/dbus_next/message_bus.py000066400000000000000000001210361407735772700216340ustar00rootroot00000000000000from ._private.address import get_bus_address, parse_address from ._private.util import replace_fds_with_idx, replace_idx_with_fds from .message import Message from .constants import BusType, MessageFlag, MessageType, ErrorType, NameFlag, RequestNameReply, ReleaseNameReply from .service import ServiceInterface from .validators import assert_object_path_valid, assert_bus_name_valid from .errors import DBusError, InvalidAddressError from .signature import Variant from .proxy_object import BaseProxyObject from . import introspection as intr import inspect import socket import logging import xml.etree.ElementTree as ET import traceback from typing import Type, Callable, Optional, Union class BaseMessageBus: """An abstract class to manage a connection to a DBus message bus. The message bus class is the entry point into all the features of the library. It sets up a connection to the DBus daemon and exposes an interface to send and receive messages and expose services. This class is not meant to be used directly by users. For more information, see the documentation for the implementation of the message bus you plan to use. :param bus_type: The type of bus to connect to. Affects the search path for the bus address. :type bus_type: :class:`BusType ` :param bus_address: A specific bus address to connect to. Should not be used under normal circumstances. :type bus_address: str :param ProxyObject: The proxy object implementation for this message bus. Must be passed in by an implementation that supports the high-level client. :type ProxyObject: Type[:class:`BaseProxyObject `] :ivar unique_name: The unique name of the message bus connection. It will be :class:`None` until the message bus connects. :vartype unique_name: str :ivar connected: True if this message bus is expected to be able to send and receive messages. :vartype connected: bool """ def __init__(self, bus_address: Optional[str] = None, bus_type: BusType = BusType.SESSION, ProxyObject: Optional[Type[BaseProxyObject]] = None): self.unique_name = None self._disconnected = False # True if the user disconnected himself, so don't throw errors out of # the main loop. self._user_disconnect = False self._method_return_handlers = {} self._serial = 0 self._user_message_handlers = [] # the key is the name and the value is the unique name of the owner. # This cache is kept up to date by the NameOwnerChanged signal and is # used to route messages to the correct proxy object. (used for the # high level client only) self._name_owners = {} # used for the high level service self._path_exports = {} self._bus_address = parse_address(bus_address) if bus_address else parse_address( get_bus_address(bus_type)) # the bus implementations need this rule for the high level client to # work correctly. self._name_owner_match_rule = "sender='org.freedesktop.DBus',interface='org.freedesktop.DBus',path='/org/freedesktop/DBus',member='NameOwnerChanged'" # _match_rules: the keys are match rules and the values are ref counts # (used for the high level client only) self._match_rules = {} self._high_level_client_initialized = False self._ProxyObject = ProxyObject # machine id is lazy loaded self._machine_id = None self._setup_socket() @property def connected(self): if self.unique_name is None or self._disconnected or self._user_disconnect: return False return True def export(self, path: str, interface: ServiceInterface): """Export the service interface on this message bus to make it available to other clients. :param path: The object path to export this interface on. :type path: str :param interface: The service interface to export. :type interface: :class:`ServiceInterface ` :raises: - :class:`InvalidObjectPathError ` - If the given object path is not valid. - :class:`ValueError` - If an interface with this name is already exported on the message bus at this path """ assert_object_path_valid(path) if not isinstance(interface, ServiceInterface): raise TypeError('interface must be a ServiceInterface') if path not in self._path_exports: self._path_exports[path] = [] for f in self._path_exports[path]: if f.name == interface.name: raise ValueError( f'An interface with this name is already exported on this bus at path "{path}": "{interface.name}"' ) self._path_exports[path].append(interface) ServiceInterface._add_bus(interface, self) self._emit_interface_added(path, interface) def unexport(self, path: str, interface: Optional[Union[ServiceInterface, str]] = None): """Unexport the path or service interface to make it no longer available to clients. :param path: The object path to unexport. :type path: str :param interface: The interface instance or the name of the interface to unexport. If ``None``, unexport every interface on the path. :type interface: :class:`ServiceInterface ` or str or None :raises: - :class:`InvalidObjectPathError ` - If the given object path is not valid. """ assert_object_path_valid(path) if type(interface) not in [str, type(None)] and not isinstance(interface, ServiceInterface): raise TypeError('interface must be a ServiceInterface or interface name') if path not in self._path_exports: return exports = self._path_exports[path] if type(interface) is str: try: interface = next(iface for iface in exports if iface.name == interface) except StopIteration: return removed_interfaces = [] if interface is None: del self._path_exports[path] for iface in filter(lambda e: not self._has_interface(e), exports): removed_interfaces.append(iface.name) ServiceInterface._remove_bus(iface, self) else: for i, iface in enumerate(exports): if iface is interface: removed_interfaces.append(iface.name) del self._path_exports[path][i] if not self._path_exports[path]: del self._path_exports[path] if not self._has_interface(iface): ServiceInterface._remove_bus(iface, self) break self._emit_interface_removed(path, removed_interfaces) def introspect(self, bus_name: str, path: str, callback: Callable[[Optional[intr.Node], Optional[Exception]], None]): """Get introspection data for the node at the given path from the given bus name. Calls the standard ``org.freedesktop.DBus.Introspectable.Introspect`` on the bus for the path. :param bus_name: The name to introspect. :type bus_name: str :param path: The path to introspect. :type path: str :param callback: A callback that will be called with the introspection data as a :class:`Node `. :type callback: :class:`Callable` :raises: - :class:`InvalidObjectPathError ` - If the given object path is not valid. - :class:`InvalidBusNameError ` - If the given bus name is not valid. """ BaseMessageBus._check_callback_type(callback) def reply_notify(reply, err): try: BaseMessageBus._check_method_return(reply, err, 's') result = intr.Node.parse(reply.body[0]) except Exception as e: callback(None, e) return callback(result, None) self._call( Message(destination=bus_name, path=path, interface='org.freedesktop.DBus.Introspectable', member='Introspect'), reply_notify) def _emit_interface_added(self, path, interface): """Emit the ``org.freedesktop.DBus.ObjectManager.InterfacesAdded`` signal. This signal is intended to be used to alert clients when a new interface has been added. :param path: Path of exported object. :type path: str :param interface: Exported service interface. :type interface: :class:`ServiceInterface ` """ if self._disconnected: return def get_properties_callback(interface, result, user_data, e): if e is not None: try: raise e except Exception: logging.error( 'An exception ocurred when emitting ObjectManager.InterfacesAdded for %s. ' 'Some properties will not be included in the signal.', interface.name, exc_info=True) body = {interface.name: result} self.send( Message.new_signal(path=path, interface='org.freedesktop.DBus.ObjectManager', member='InterfacesAdded', signature='oa{sa{sv}}', body=[path, body])) ServiceInterface._get_all_property_values(interface, get_properties_callback) def _emit_interface_removed(self, path, removed_interfaces): """Emit the ``org.freedesktop.DBus.ObjectManager.InterfacesRemoved` signal. This signal is intended to be used to alert clients when a interface has been removed. :param path: Path of removed (unexported) object. :type path: str :param removed_interfaces: List of unexported service interfaces. :type removed_interfaces: list[str] """ if self._disconnected: return self.send( Message.new_signal(path=path, interface='org.freedesktop.DBus.ObjectManager', member='InterfacesRemoved', signature='oas', body=[path, removed_interfaces])) def request_name(self, name: str, flags: NameFlag = NameFlag.NONE, callback: Optional[Callable[[Optional[RequestNameReply], Optional[Exception]], None]] = None): """Request that this message bus owns the given name. :param name: The name to request. :type name: str :param flags: Name flags that affect the behavior of the name request. :type flags: :class:`NameFlag ` :param callback: A callback that will be called with the reply of the request as a :class:`RequestNameReply `. :type callback: :class:`Callable` :raises: - :class:`InvalidBusNameError ` - If the given bus name is not valid. """ assert_bus_name_valid(name) if callback is not None: BaseMessageBus._check_callback_type(callback) def reply_notify(reply, err): try: BaseMessageBus._check_method_return(reply, err, 'u') result = RequestNameReply(reply.body[0]) except Exception as e: callback(None, e) return callback(result, None) if type(flags) is not NameFlag: flags = NameFlag(flags) self._call( Message(destination='org.freedesktop.DBus', path='/org/freedesktop/DBus', interface='org.freedesktop.DBus', member='RequestName', signature='su', body=[name, flags]), reply_notify if callback else None) def release_name(self, name: str, callback: Optional[Callable[[Optional[ReleaseNameReply], Optional[Exception]], None]] = None): """Request that this message bus release the given name. :param name: The name to release. :type name: str :param callback: A callback that will be called with the reply of the release request as a :class:`ReleaseNameReply `. :type callback: :class:`Callable` :raises: - :class:`InvalidBusNameError ` - If the given bus name is not valid. """ assert_bus_name_valid(name) if callback is not None: BaseMessageBus._check_callback_type(callback) def reply_notify(reply, err): try: BaseMessageBus._check_method_return(reply, err, 'u') result = ReleaseNameReply(reply.body[0]) except Exception as e: callback(None, e) return callback(result, None) self._call( Message(destination='org.freedesktop.DBus', path='/org/freedesktop/DBus', interface='org.freedesktop.DBus', member='ReleaseName', signature='s', body=[name]), reply_notify if callback else None) def get_proxy_object(self, bus_name: str, path: str, introspection: Union[intr.Node, str, ET.Element]) -> BaseProxyObject: """Get a proxy object for the path exported on the bus that owns the name. The object is expected to export the interfaces and nodes specified in the introspection data. This is the entry point into the high-level client. :param bus_name: The name on the bus to get the proxy object for. :type bus_name: str :param path: The path on the client for the proxy object. :type path: str :param introspection: XML introspection data used to build the interfaces on the proxy object. :type introspection: :class:`Node ` or str or :class:`ElementTree` :returns: A proxy object for the given path on the given name. :rtype: :class:`BaseProxyObject ` :raises: - :class:`InvalidBusNameError ` - If the given bus name is not valid. - :class:`InvalidObjectPathError ` - If the given object path is not valid. - :class:`InvalidIntrospectionError ` - If the introspection data for the node is not valid. """ if self._ProxyObject is None: raise Exception('the message bus implementation did not provide a proxy object class') self._init_high_level_client() return self._ProxyObject(bus_name, path, introspection, self) def disconnect(self): """Disconnect the message bus by closing the underlying connection asynchronously. All pending and future calls will error with a connection error. """ self._user_disconnect = True try: self._sock.shutdown(socket.SHUT_RDWR) except Exception: logging.warning('could not shut down socket', exc_info=True) def next_serial(self) -> int: """Get the next serial for this bus. This can be used as the ``serial`` attribute of a :class:`Message ` to manually handle the serial of messages. :returns: The next serial for the bus. :rtype: int """ self._serial += 1 return self._serial def add_message_handler(self, handler: Callable[[Message], Optional[Union[Message, bool]]]): """Add a custom message handler for incoming messages. The handler should be a callable that takes a :class:`Message `. If the message is a method call, you may return another Message as a reply and it will be marked as handled. You may also return ``True`` to mark the message as handled without sending a reply. :param handler: A handler that will be run for every message the bus connection received. :type handler: :class:`Callable` or None """ error_text = 'a message handler must be callable with a single parameter' if not callable(handler): raise TypeError(error_text) handler_signature = inspect.signature(handler) if len(handler_signature.parameters) != 1: raise TypeError(error_text) self._user_message_handlers.append(handler) def remove_message_handler(self, handler: Callable[[Message], Optional[Union[Message, bool]]]): """Remove a message handler that was previously added by :func:`add_message_handler() `. :param handler: A message handler. :type handler: :class:`Callable` """ for i, h in enumerate(self._user_message_handlers): if h == handler: del self._user_message_handlers[i] break def send(self, msg: Message) -> None: """Asynchronously send a message on the message bus. :param msg: The message to send. :type msg: :class:`Message ` """ raise NotImplementedError('the "send" method must be implemented in the inheriting class') def _finalize(self, err): '''should be called after the socket disconnects with the disconnection error to clean up resources and put the bus in a disconnected state''' if self._disconnected: return self._disconnected = True for handler in self._method_return_handlers.values(): try: handler(None, err) except Exception: logging.warning('a message handler threw an exception on shutdown', exc_info=True) self._method_return_handlers.clear() for path in list(self._path_exports.keys()): self.unexport(path) self._user_message_handlers.clear() def _has_interface(self, interface: ServiceInterface) -> bool: for _, exports in self._path_exports.items(): for iface in exports: if iface is interface: return True return False def _interface_signal_notify(self, interface, interface_name, member, signature, body, unix_fds=[]): path = None for p, ifaces in self._path_exports.items(): for i in ifaces: if i is interface: path = p if path is None: raise Exception('Could not find interface on bus (this is a bug in dbus-next)') self.send( Message.new_signal(path=path, interface=interface_name, member=member, signature=signature, body=body, unix_fds=unix_fds)) def _introspect_export_path(self, path): assert_object_path_valid(path) if path in self._path_exports: node = intr.Node.default(path) for interface in self._path_exports[path]: node.interfaces.append(interface.introspect()) else: node = intr.Node(path) children = set() for export_path in self._path_exports: try: child_path = export_path.split(path, maxsplit=1)[1] except IndexError: continue child_path = child_path.lstrip('/') child_name = child_path.split('/', maxsplit=1)[0] children.add(child_name) node.nodes = [intr.Node(name) for name in children if name] return node def _setup_socket(self): err = None for transport, options in self._bus_address: filename = None ip_addr = '' ip_port = 0 if transport == 'unix': self._sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) self._stream = self._sock.makefile('rwb') self._fd = self._sock.fileno() if 'path' in options: filename = options['path'] elif 'abstract' in options: filename = f'\0{options["abstract"]}' else: raise InvalidAddressError('got unix transport with unknown path specifier') try: self._sock.connect(filename) self._sock.setblocking(False) break except Exception as e: err = e elif transport == 'tcp': self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self._stream = self._sock.makefile('rwb') self._fd = self._sock.fileno() if 'host' in options: ip_addr = options['host'] if 'port' in options: ip_port = int(options['port']) try: self._sock.connect((ip_addr, ip_port)) self._sock.setblocking(False) break except Exception as e: err = e else: raise InvalidAddressError(f'got unknown address transport: {transport}') if err: raise err def _call(self, msg, callback): BaseMessageBus._check_callback_type(callback) if not msg.serial: msg.serial = self.next_serial() def reply_notify(reply, err): if reply: self._name_owners[msg.destination] = reply.sender callback(reply, err) self.send(msg) if msg.flags & MessageFlag.NO_REPLY_EXPECTED: callback(None, None) else: self._method_return_handlers[msg.serial] = reply_notify @staticmethod def _check_callback_type(callback): """Raise a TypeError if the user gives an invalid callback as a parameter""" text = 'a callback must be callable with two parameters' if not callable(callback): raise TypeError(text) fn_signature = inspect.signature(callback) if len(fn_signature.parameters) != 2: raise TypeError(text) @staticmethod def _check_method_return(msg, err, signature): if err: raise err elif msg.message_type == MessageType.METHOD_RETURN and msg.signature == signature: return elif msg.message_type == MessageType.ERROR: raise DBusError._from_message(msg) else: raise DBusError(ErrorType.INTERNAL_ERROR, 'invalid message type for method call', msg) def _on_message(self, msg): try: self._process_message(msg) except Exception as e: logging.error( f'got unexpected error processing a message: {e}.\n{traceback.format_exc()}') def _send_reply(self, msg): bus = self class SendReply: def __enter__(self): return self def __call__(self, reply): if msg.flags & MessageFlag.NO_REPLY_EXPECTED: return bus.send(reply) def _exit(self, exc_type, exc_value, tb): if exc_type is None: return if issubclass(exc_type, DBusError): self(exc_value._as_message(msg)) return True if issubclass(exc_type, Exception): self( Message.new_error( msg, ErrorType.SERVICE_ERROR, f'The service interface raised an error: {exc_value}.\n{traceback.format_tb(tb)}' )) return True def __exit__(self, exc_type, exc_value, tb): self._exit(exc_type, exc_value, tb) def send_error(self, exc): self._exit(exc.__class__, exc, exc.__traceback__) return SendReply() def _process_message(self, msg): handled = False for handler in self._user_message_handlers: try: result = handler(msg) if result: if type(result) is Message: self.send(result) handled = True break except DBusError as e: if msg.message_type == MessageType.METHOD_CALL: self.send(e._as_message(msg)) handled = True break else: logging.error( f'A message handler raised an exception: {e}.\n{traceback.format_exc()}') except Exception as e: logging.error( f'A message handler raised an exception: {e}.\n{traceback.format_exc()}') if msg.message_type == MessageType.METHOD_CALL: self.send( Message.new_error( msg, ErrorType.INTERNAL_ERROR, f'An internal error occurred: {e}.\n{traceback.format_exc()}')) handled = True break if msg.message_type == MessageType.SIGNAL: if msg._matches(sender='org.freedesktop.DBus', path='/org/freedesktop/DBus', interface='org.freedesktop.DBus', member='NameOwnerChanged'): [name, old_owner, new_owner] = msg.body if new_owner: self._name_owners[name] = new_owner elif name in self._name_owners: del self._name_owners[name] elif msg.message_type == MessageType.METHOD_CALL: if not handled: handler = self._find_message_handler(msg) send_reply = self._send_reply(msg) with send_reply: if handler: handler(msg, send_reply) else: send_reply( Message.new_error( msg, ErrorType.UNKNOWN_METHOD, f'{msg.interface}.{msg.member} with signature "{msg.signature}" could not be found' )) else: # An ERROR or a METHOD_RETURN if msg.reply_serial in self._method_return_handlers: if not handled: handler = self._method_return_handlers[msg.reply_serial] handler(msg, None) del self._method_return_handlers[msg.reply_serial] def _make_method_handler(self, interface, method): def handler(msg, send_reply): args = ServiceInterface._msg_body_to_args(msg) result = method.fn(interface, *args) body, fds = ServiceInterface._fn_result_to_body( result, signature_tree=method.out_signature_tree) send_reply(Message.new_method_return(msg, method.out_signature, body, fds)) return handler def _find_message_handler(self, msg): handler = None if msg._matches(interface='org.freedesktop.DBus.Introspectable', member='Introspect', signature=''): handler = self._default_introspect_handler elif msg._matches(interface='org.freedesktop.DBus.Properties'): handler = self._default_properties_handler elif msg._matches(interface='org.freedesktop.DBus.Peer'): if msg._matches(member='Ping', signature=''): handler = self._default_ping_handler elif msg._matches(member='GetMachineId', signature=''): handler = self._default_get_machine_id_handler elif msg._matches(interface='org.freedesktop.DBus.ObjectManager', member='GetManagedObjects'): handler = self._default_get_managed_objects_handler else: for interface in self._path_exports.get(msg.path, []): for method in ServiceInterface._get_methods(interface): if method.disabled: continue if msg._matches(interface=interface.name, member=method.name, signature=method.in_signature): handler = self._make_method_handler(interface, method) break if handler: break return handler def _default_introspect_handler(self, msg, send_reply): introspection = self._introspect_export_path(msg.path).tostring() send_reply(Message.new_method_return(msg, 's', [introspection])) def _default_ping_handler(self, msg, send_reply): send_reply(Message.new_method_return(msg)) def _default_get_machine_id_handler(self, msg, send_reply): if self._machine_id: send_reply(Message.new_method_return(msg, 's', self._machine_id)) return def reply_handler(reply, err): if err: # the bus has been disconnected, cannot send a reply return if reply.message_type == MessageType.METHOD_RETURN: self._machine_id = reply.body[0] send_reply(Message.new_method_return(msg, 's', [self._machine_id])) elif reply.message_type == MessageType.ERROR: send_reply(Message.new_error(msg, reply.error_name, reply.body)) else: send_reply(Message.new_error(msg, ErrorType.FAILED, 'could not get machine_id')) self._call( Message(destination='org.freedesktop.DBus', path='/org/freedesktop/DBus', interface='org.freedesktop.DBus.Peer', member='GetMachineId'), reply_handler) def _default_get_managed_objects_handler(self, msg, send_reply): result = {} result_signature = 'a{oa{sa{sv}}}' error_handled = False def is_result_complete(): if not result: return True for n, interfaces in result.items(): for value in interfaces.values(): if value is None: return False return True nodes = [ node for node in self._path_exports if msg.path == '/' or node.startswith(msg.path + '/') ] # first build up the result object to know when it's complete for node in nodes: result[node] = {} for interface in self._path_exports[node]: result[node][interface.name] = None if is_result_complete(): send_reply(Message.new_method_return(msg, result_signature, [result])) return def get_all_properties_callback(interface, values, node, err): nonlocal error_handled if err is not None: if not error_handled: error_handled = True send_reply.send_error(err) return result[node][interface.name] = values if is_result_complete(): send_reply(Message.new_method_return(msg, result_signature, [result])) for node in nodes: for interface in self._path_exports[node]: ServiceInterface._get_all_property_values(interface, get_all_properties_callback, node) def _default_properties_handler(self, msg, send_reply): methods = {'Get': 'ss', 'Set': 'ssv', 'GetAll': 's'} if msg.member not in methods or methods[msg.member] != msg.signature: raise DBusError( ErrorType.UNKNOWN_METHOD, f'properties interface doesn\'t have method "{msg.member}" with signature "{msg.signature}"' ) interface_name = msg.body[0] if interface_name == '': raise DBusError( ErrorType.NOT_SUPPORTED, 'getting and setting properties with an empty interface string is not supported yet' ) elif msg.path not in self._path_exports: raise DBusError(ErrorType.UNKNOWN_OBJECT, f'no interfaces at path: "{msg.path}"') match = [iface for iface in self._path_exports[msg.path] if iface.name == interface_name] if not match: if interface_name in [ 'org.freedesktop.DBus.Properties', 'org.freedesktop.DBus.Introspectable', 'org.freedesktop.DBus.Peer', 'org.freedesktop.DBus.ObjectManager' ]: # the standard interfaces do not have properties if msg.member == 'Get' or msg.member == 'Set': prop_name = msg.body[1] raise DBusError( ErrorType.UNKNOWN_PROPERTY, f'interface "{interface_name}" does not have property "{prop_name}"') elif msg.member == 'GetAll': send_reply(Message.new_method_return(msg, 'a{sv}', [{}])) return else: assert False raise DBusError( ErrorType.UNKNOWN_INTERFACE, f'could not find an interface "{interface_name}" at path: "{msg.path}"') interface = match[0] properties = ServiceInterface._get_properties(interface) if msg.member == 'Get' or msg.member == 'Set': prop_name = msg.body[1] match = [prop for prop in properties if prop.name == prop_name and not prop.disabled] if not match: raise DBusError( ErrorType.UNKNOWN_PROPERTY, f'interface "{interface_name}" does not have property "{prop_name}"') prop = match[0] if msg.member == 'Get': if not prop.access.readable(): raise DBusError(ErrorType.UNKNOWN_PROPERTY, 'the property does not have read access') def get_property_callback(interface, prop, prop_value, err): try: if err is not None: send_reply.send_error(err) return body, unix_fds = replace_fds_with_idx(prop.signature, [prop_value]) send_reply( Message.new_method_return(msg, 'v', [Variant(prop.signature, body[0])], unix_fds=unix_fds)) except Exception as e: send_reply.send_error(e) ServiceInterface._get_property_value(interface, prop, get_property_callback) elif msg.member == 'Set': if not prop.access.writable(): raise DBusError(ErrorType.PROPERTY_READ_ONLY, 'the property is readonly') value = msg.body[2] if value.signature != prop.signature: raise DBusError(ErrorType.INVALID_SIGNATURE, f'wrong signature for property. expected "{prop.signature}"') assert prop.prop_setter def set_property_callback(interface, prop, err): if err is not None: send_reply.send_error(err) return send_reply(Message.new_method_return(msg)) body = replace_idx_with_fds(value.signature, [value.value], msg.unix_fds) ServiceInterface._set_property_value(interface, prop, body[0], set_property_callback) elif msg.member == 'GetAll': def get_all_properties_callback(interface, values, user_data, err): if err is not None: send_reply.send_error(err) return body, unix_fds = replace_fds_with_idx('a{sv}', [values]) send_reply(Message.new_method_return(msg, 'a{sv}', body, unix_fds=unix_fds)) ServiceInterface._get_all_property_values(interface, get_all_properties_callback) else: assert False def _init_high_level_client(self): '''The high level client is initialized when the first proxy object is gotten. Currently just sets up the match rules for the name owner cache so signals can be routed to the right objects.''' if self._high_level_client_initialized: return self._high_level_client_initialized = True def add_match_notify(msg, err): if err: logging.error( f'add match request failed. match="{self._name_owner_match_rule}", {err}') if msg.message_type == MessageType.ERROR: logging.error( f'add match request failed. match="{self._name_owner_match_rule}", {msg.body[0]}' ) self._call( Message(destination='org.freedesktop.DBus', interface='org.freedesktop.DBus', path='/org/freedesktop/DBus', member='AddMatch', signature='s', body=[self._name_owner_match_rule]), add_match_notify) def _add_match_rule(self, match_rule): '''Add a match rule. Match rules added by this function are refcounted and must be removed by _remove_match_rule(). This is for use in the high level client only.''' if match_rule == self._name_owner_match_rule: return if match_rule in self._match_rules: self._match_rules[match_rule] += 1 return self._match_rules[match_rule] = 1 def add_match_notify(msg, err): if err: logging.error(f'add match request failed. match="{match_rule}", {err}') if msg.message_type == MessageType.ERROR: logging.error(f'add match request failed. match="{match_rule}", {msg.body[0]}') self._call( Message(destination='org.freedesktop.DBus', interface='org.freedesktop.DBus', path='/org/freedesktop/DBus', member='AddMatch', signature='s', body=[match_rule]), add_match_notify) def _remove_match_rule(self, match_rule): '''Remove a match rule added with _add_match_rule(). This is for use in the high level client only.''' if match_rule == self._name_owner_match_rule: return if match_rule in self._match_rules: self._match_rules[match_rule] -= 1 if self._match_rules[match_rule] > 0: return del self._match_rules[match_rule] def remove_match_notify(msg, err): if self._disconnected: return if err: logging.error(f'remove match request failed. match="{match_rule}", {err}') if msg.message_type == MessageType.ERROR: logging.error(f'remove match request failed. match="{match_rule}", {msg.body[0]}') self._call( Message(destination='org.freedesktop.DBus', interface='org.freedesktop.DBus', path='/org/freedesktop/DBus', member='RemoveMatch', signature='s', body=[match_rule]), remove_match_notify) python-dbus-next-0.2.3/dbus_next/proxy_object.py000066400000000000000000000273211407735772700220500ustar00rootroot00000000000000from .validators import assert_object_path_valid, assert_bus_name_valid from . import message_bus from .message import Message from .constants import MessageType, ErrorType from . import introspection as intr from .errors import DBusError, InterfaceNotFoundError from ._private.util import replace_idx_with_fds from typing import Type, Union, List, Coroutine import logging import xml.etree.ElementTree as ET import inspect import re import asyncio class BaseProxyInterface: """An abstract class representing a proxy to an interface exported on the bus by another client. Implementations of this class are not meant to be constructed directly by users. Use :func:`BaseProxyObject.get_interface` to get a proxy interface. Each message bus implementation provides its own proxy interface implementation that will be returned by that method. Proxy interfaces can be used to call methods, get properties, and listen to signals on the interface. Proxy interfaces are created dynamically with a family of methods for each of these operations based on what members the interface exposes. Each proxy interface implementation exposes these members in a different way depending on the features of the backend. See the documentation of the proxy interface implementation you use for more details. :ivar bus_name: The name of the bus this interface is exported on. :vartype bus_name: str :ivar path: The object path exported on the client that owns the bus name. :vartype path: str :ivar introspection: Parsed introspection data for the proxy interface. :vartype introspection: :class:`Node ` :ivar bus: The message bus this proxy interface is connected to. :vartype bus: :class:`BaseMessageBus ` """ def __init__(self, bus_name, path, introspection, bus): self.bus_name = bus_name self.path = path self.introspection = introspection self.bus = bus self._signal_handlers = {} self._signal_match_rule = f"type='signal',sender={bus_name},interface={introspection.name},path={path}" _underscorer1 = re.compile(r'(.)([A-Z][a-z]+)') _underscorer2 = re.compile(r'([a-z0-9])([A-Z])') @staticmethod def _to_snake_case(member): subbed = BaseProxyInterface._underscorer1.sub(r'\1_\2', member) return BaseProxyInterface._underscorer2.sub(r'\1_\2', subbed).lower() @staticmethod def _check_method_return(msg, signature=None): if msg.message_type == MessageType.ERROR: raise DBusError._from_message(msg) elif msg.message_type != MessageType.METHOD_RETURN: raise DBusError(ErrorType.CLIENT_ERROR, 'method call didnt return a method return', msg) elif signature is not None and msg.signature != signature: raise DBusError(ErrorType.CLIENT_ERROR, f'method call returned unexpected signature: "{msg.signature}"', msg) def _add_method(self, intr_method): raise NotImplementedError('this must be implemented in the inheriting class') def _add_property(self, intr_property): raise NotImplementedError('this must be implemented in the inheriting class') def _message_handler(self, msg): if not msg._matches(message_type=MessageType.SIGNAL, interface=self.introspection.name, path=self.path) or msg.member not in self._signal_handlers: return if msg.sender != self.bus_name and self.bus._name_owners.get(self.bus_name, '') != msg.sender: # The sender is always a unique name, but the bus name given might # be a well known name. If the sender isn't an exact match, check # to see if it owns the bus_name we were given from the cache kept # on the bus for this purpose. return match = [s for s in self.introspection.signals if s.name == msg.member] if not len(match): return intr_signal = match[0] if intr_signal.signature != msg.signature: logging.warning( f'got signal "{self.introspection.name}.{msg.member}" with unexpected signature "{msg.signature}"' ) return body = replace_idx_with_fds(msg.signature, msg.body, msg.unix_fds) for handler in self._signal_handlers[msg.member]: cb_result = handler(*body) if isinstance(cb_result, Coroutine): asyncio.create_task(cb_result) def _add_signal(self, intr_signal, interface): def on_signal_fn(fn): fn_signature = inspect.signature(fn) if not callable(fn) or len(fn_signature.parameters) != len(intr_signal.args): raise TypeError( f'reply_notify must be a function with {len(intr_signal.args)} parameters') if not self._signal_handlers: self.bus._add_match_rule(self._signal_match_rule) self.bus.add_message_handler(self._message_handler) if intr_signal.name not in self._signal_handlers: self._signal_handlers[intr_signal.name] = [] self._signal_handlers[intr_signal.name].append(fn) def off_signal_fn(fn): try: i = self._signal_handlers[intr_signal.name].index(fn) del self._signal_handlers[intr_signal.name][i] if not self._signal_handlers[intr_signal.name]: del self._signal_handlers[intr_signal.name] except (KeyError, ValueError): return if not self._signal_handlers: self.bus._remove_match_rule(self._signal_match_rule) self.bus.remove_message_handler(self._message_handler) snake_case = BaseProxyInterface._to_snake_case(intr_signal.name) setattr(interface, f'on_{snake_case}', on_signal_fn) setattr(interface, f'off_{snake_case}', off_signal_fn) class BaseProxyObject: """An abstract class representing a proxy to an object exported on the bus by another client. Implementations of this class are not meant to be constructed directly. Use :func:`BaseMessageBus.get_proxy_object() ` to get a proxy object. Each message bus implementation provides its own proxy object implementation that will be returned by that method. The primary use of the proxy object is to select a proxy interface to act on. Information on what interfaces are available is provided by introspection data provided to this class. This introspection data can either be included in your project as an XML file (recommended) or retrieved from the ``org.freedesktop.DBus.Introspectable`` interface at runtime. :ivar bus_name: The name of the bus this object is exported on. :vartype bus_name: str :ivar path: The object path exported on the client that owns the bus name. :vartype path: str :ivar introspection: Parsed introspection data for the proxy object. :vartype introspection: :class:`Node ` :ivar bus: The message bus this proxy object is connected to. :vartype bus: :class:`BaseMessageBus ` :ivar ~.ProxyInterface: The proxy interface class this proxy object uses. :vartype ~.ProxyInterface: Type[:class:`BaseProxyInterface `] :ivar child_paths: A list of absolute object paths of the children of this object. :vartype child_paths: list(str) :raises: - :class:`InvalidBusNameError ` - If the given bus name is not valid. - :class:`InvalidObjectPathError ` - If the given object path is not valid. - :class:`InvalidIntrospectionError ` - If the introspection data for the node is not valid. """ def __init__(self, bus_name: str, path: str, introspection: Union[intr.Node, str, ET.Element], bus: 'message_bus.BaseMessageBus', ProxyInterface: Type[BaseProxyInterface]): assert_object_path_valid(path) assert_bus_name_valid(bus_name) if not isinstance(bus, message_bus.BaseMessageBus): raise TypeError('bus must be an instance of BaseMessageBus') if not issubclass(ProxyInterface, BaseProxyInterface): raise TypeError('ProxyInterface must be an instance of BaseProxyInterface') if type(introspection) is intr.Node: self.introspection = introspection elif type(introspection) is str: self.introspection = intr.Node.parse(introspection) elif type(introspection) is ET.Element: self.introspection = intr.Node.from_xml(introspection) else: raise TypeError( 'introspection must be xml node introspection or introspection.Node class') self.bus_name = bus_name self.path = path self.bus = bus self.ProxyInterface = ProxyInterface self.child_paths = [f'{path}/{n.name}' for n in self.introspection.nodes] self._interfaces = {} # lazy loaded by get_children() self._children = None def get_interface(self, name: str) -> BaseProxyInterface: """Get an interface exported on this proxy object and connect it to the bus. :param name: The name of the interface to retrieve. :type name: str :raises: - :class:`InterfaceNotFoundError ` - If there is no interface by this name exported on the bus. """ if name in self._interfaces: return self._interfaces[name] try: intr_interface = next(i for i in self.introspection.interfaces if i.name == name) except StopIteration: raise InterfaceNotFoundError(f'interface not found on this object: {name}') interface = self.ProxyInterface(self.bus_name, self.path, intr_interface, self.bus) for intr_method in intr_interface.methods: interface._add_method(intr_method) for intr_property in intr_interface.properties: interface._add_property(intr_property) for intr_signal in intr_interface.signals: interface._add_signal(intr_signal, interface) def get_owner_notify(msg, err): if err: logging.error(f'getting name owner for "{name}" failed, {err}') return if msg.message_type == MessageType.ERROR: if msg.error_name != ErrorType.NAME_HAS_NO_OWNER.value: logging.error(f'getting name owner for "{name}" failed, {msg.body[0]}') return self.bus._name_owners[self.bus_name] = msg.body[0] if self.bus_name[0] != ':' and not self.bus._name_owners.get(self.bus_name, ''): self.bus._call( Message(destination='org.freedesktop.DBus', interface='org.freedesktop.DBus', path='/org/freedesktop/DBus', member='GetNameOwner', signature='s', body=[self.bus_name]), get_owner_notify) self._interfaces[name] = interface return interface def get_children(self) -> List['BaseProxyObject']: """Get the child nodes of this proxy object according to the introspection data.""" if self._children is None: self._children = [ self.__class__(self.bus_name, self.path, child, self.bus) for child in self.introspection.nodes ] return self._children python-dbus-next-0.2.3/dbus_next/py.typed000066400000000000000000000000001407735772700204470ustar00rootroot00000000000000python-dbus-next-0.2.3/dbus_next/service.py000066400000000000000000000501421407735772700207760ustar00rootroot00000000000000from .constants import PropertyAccess from .signature import SignatureTree, SignatureBodyMismatchError, Variant from . import introspection as intr from .errors import SignalDisabledError from ._private.util import signature_contains_type, replace_fds_with_idx, replace_idx_with_fds, parse_annotation from functools import wraps import inspect from typing import no_type_check_decorator, Dict, List, Any import copy import asyncio class _Method: def __init__(self, fn, name, disabled=False): in_signature = '' out_signature = '' inspection = inspect.signature(fn) in_args = [] for i, param in enumerate(inspection.parameters.values()): if i == 0: # first is self continue annotation = parse_annotation(param.annotation) if not annotation: raise ValueError( 'method parameters must specify the dbus type string as an annotation') in_args.append(intr.Arg(annotation, intr.ArgDirection.IN, param.name)) in_signature += annotation out_args = [] out_signature = parse_annotation(inspection.return_annotation) if out_signature: for type_ in SignatureTree._get(out_signature).types: out_args.append(intr.Arg(type_, intr.ArgDirection.OUT)) self.name = name self.fn = fn self.disabled = disabled self.introspection = intr.Method(name, in_args, out_args) self.in_signature = in_signature self.out_signature = out_signature self.in_signature_tree = SignatureTree._get(in_signature) self.out_signature_tree = SignatureTree._get(out_signature) def method(name: str = None, disabled: bool = False): """A decorator to mark a class method of a :class:`ServiceInterface` to be a DBus service method. The parameters and return value must each be annotated with a signature string of a single complete DBus type. This class method will be called when a client calls the method on the DBus interface. The parameters given to the function come from the calling client and will conform to the dbus-next type system. The parameters returned will be returned to the calling client and must conform to the dbus-next type system. If multiple parameters are returned, they must be contained within a :class:`list`. The decorated method may raise a :class:`DBusError ` to return an error to the client. :param name: The member name that DBus clients will use to call this method. Defaults to the name of the class method. :type name: str :param disabled: If set to true, the method will not be visible to clients. :type disabled: bool :example: :: @method() def echo(self, val: 's') -> 's': return val @method() def echo_two(self, val1: 's', val2: 'u') -> 'su': return [val1, val2] """ if name is not None and type(name) is not str: raise TypeError('name must be a string') if type(disabled) is not bool: raise TypeError('disabled must be a bool') @no_type_check_decorator def decorator(fn): @wraps(fn) def wrapped(*args, **kwargs): fn(*args, **kwargs) fn_name = name if name else fn.__name__ wrapped.__dict__['__DBUS_METHOD'] = _Method(fn, fn_name, disabled=disabled) return wrapped return decorator class _Signal: def __init__(self, fn, name, disabled=False): inspection = inspect.signature(fn) args = [] signature = '' signature_tree = None return_annotation = parse_annotation(inspection.return_annotation) if return_annotation: signature = return_annotation signature_tree = SignatureTree._get(signature) for type_ in signature_tree.types: args.append(intr.Arg(type_, intr.ArgDirection.OUT)) else: signature = '' signature_tree = SignatureTree._get('') self.signature = signature self.signature_tree = signature_tree self.name = name self.disabled = disabled self.introspection = intr.Signal(self.name, args) def signal(name: str = None, disabled: bool = False): """A decorator to mark a class method of a :class:`ServiceInterface` to be a DBus signal. The signal is broadcast on the bus when the decorated class method is called by the user. If the signal has an out argument, the class method must have a return type annotation with a signature string of a single complete DBus type and the return value of the class method must conform to the dbus-next type system. If the signal has multiple out arguments, they must be returned within a ``list``. :param name: The member name that will be used for this signal. Defaults to the name of the class method. :type name: str :param disabled: If set to true, the signal will not be visible to clients. :type disabled: bool :example: :: @signal() def string_signal(self, val) -> 's': return val @signal() def two_strings_signal(self, val1, val2) -> 'ss': return [val1, val2] """ if name is not None and type(name) is not str: raise TypeError('name must be a string') if type(disabled) is not bool: raise TypeError('disabled must be a bool') @no_type_check_decorator def decorator(fn): fn_name = name if name else fn.__name__ signal = _Signal(fn, fn_name, disabled) @wraps(fn) def wrapped(self, *args, **kwargs): if signal.disabled: raise SignalDisabledError('Tried to call a disabled signal') result = fn(self, *args, **kwargs) ServiceInterface._handle_signal(self, signal, result) return result wrapped.__dict__['__DBUS_SIGNAL'] = signal return wrapped return decorator class _Property(property): def set_options(self, options): self.options = getattr(self, 'options', {}) for k, v in options.items(): self.options[k] = v if 'name' in options and options['name'] is not None: self.name = options['name'] else: self.name = self.prop_getter.__name__ if 'access' in options: self.access = PropertyAccess(options['access']) else: self.access = PropertyAccess.READWRITE if 'disabled' in options: self.disabled = options['disabled'] else: self.disabled = False self.introspection = intr.Property(self.name, self.signature, self.access) self.__dict__['__DBUS_PROPERTY'] = True def __init__(self, fn, *args, **kwargs): self.prop_getter = fn self.prop_setter = None inspection = inspect.signature(fn) if len(inspection.parameters) != 1: raise ValueError('the property must only have the "self" input parameter') return_annotation = parse_annotation(inspection.return_annotation) if not return_annotation: raise ValueError( 'the property must specify the dbus type string as a return annotation string') self.signature = return_annotation tree = SignatureTree._get(return_annotation) if len(tree.types) != 1: raise ValueError('the property signature must be a single complete type') self.type = tree.types[0] if 'options' in kwargs: options = kwargs['options'] self.set_options(options) del kwargs['options'] super().__init__(fn, *args, **kwargs) def setter(self, fn, **kwargs): # XXX The setter decorator seems to be recreating the class in the list # of class members and clobbering the options so we need to reset them. # Why does it do that? result = super().setter(fn, **kwargs) result.prop_setter = fn result.set_options(self.options) return result def dbus_property(access: PropertyAccess = PropertyAccess.READWRITE, name: str = None, disabled: bool = False): """A decorator to mark a class method of a :class:`ServiceInterface` to be a DBus property. The class method must be a Python getter method with a return annotation that is a signature string of a single complete DBus type. When a client gets the property through the ``org.freedesktop.DBus.Properties`` interface, the getter will be called and the resulting value will be returned to the client. If the property is writable, it must have a setter method that takes a single parameter that is annotated with the same signature. When a client sets the property through the ``org.freedesktop.DBus.Properties`` interface, the setter will be called with the value from the calling client. The parameters of the getter and the setter must conform to the dbus-next type system. The getter or the setter may raise a :class:`DBusError ` to return an error to the client. :param name: The name that DBus clients will use to interact with this property on the bus. :type name: str :param disabled: If set to true, the property will not be visible to clients. :type disabled: bool :example: :: @dbus_property() def string_prop(self) -> 's': return self._string_prop @string_prop.setter def string_prop(self, val: 's'): self._string_prop = val """ if type(access) is not PropertyAccess: raise TypeError('access must be a PropertyAccess class') if name is not None and type(name) is not str: raise TypeError('name must be a string') if type(disabled) is not bool: raise TypeError('disabled must be a bool') @no_type_check_decorator def decorator(fn): options = {'name': name, 'access': access, 'disabled': disabled} return _Property(fn, options=options) return decorator class ServiceInterface: """An abstract class that can be extended by the user to define DBus services. Instances of :class:`ServiceInterface` can be exported on a path of the bus with the :class:`export ` method of a :class:`MessageBus `. Use the :func:`@method `, :func:`@dbus_property `, and :func:`@signal ` decorators to mark class methods as DBus methods, properties, and signals respectively. :ivar name: The name of this interface as it appears to clients. Must be a valid interface name. :vartype name: str """ def __init__(self, name: str): # TODO cannot be overridden by a dbus member self.name = name self.__methods = [] self.__properties = [] self.__signals = [] self.__buses = set() for name, member in inspect.getmembers(type(self)): member_dict = getattr(member, '__dict__', {}) if type(member) is _Property: # XXX The getter and the setter may show up as different # members if they have different names. But if they have the # same name, they will be the same member. So we try to merge # them together here. I wish we could make this cleaner. found = False for prop in self.__properties: if prop.prop_getter is member.prop_getter: found = True if member.prop_setter is not None: prop.prop_setter = member.prop_setter if not found: self.__properties.append(member) elif '__DBUS_METHOD' in member_dict: method = member_dict['__DBUS_METHOD'] assert type(method) is _Method self.__methods.append(method) elif '__DBUS_SIGNAL' in member_dict: signal = member_dict['__DBUS_SIGNAL'] assert type(signal) is _Signal self.__signals.append(signal) # validate that writable properties have a setter for prop in self.__properties: if prop.access.writable() and prop.prop_setter is None: raise ValueError(f'property "{prop.name}" is writable but does not have a setter') def emit_properties_changed(self, changed_properties: Dict[str, Any], invalidated_properties: List[str] = []): """Emit the ``org.freedesktop.DBus.Properties.PropertiesChanged`` signal. This signal is intended to be used to alert clients when a property of the interface has changed. :param changed_properties: The keys must be the names of properties exposed by this bus. The values must be valid for the signature of those properties. :type changed_properties: dict(str, Any) :param invalidated_properties: A list of names of properties that are now invalid (presumably for clients who cache the value). :type invalidated_properties: list(str) """ # TODO cannot be overridden by a dbus member variant_dict = {} for prop in ServiceInterface._get_properties(self): if prop.name in changed_properties: variant_dict[prop.name] = Variant(prop.signature, changed_properties[prop.name]) body = [self.name, variant_dict, invalidated_properties] for bus in ServiceInterface._get_buses(self): bus._interface_signal_notify(self, 'org.freedesktop.DBus.Properties', 'PropertiesChanged', 'sa{sv}as', body) def introspect(self) -> intr.Interface: """Get introspection information for this interface. This might be useful for creating clients for the interface or examining the introspection output of an interface. :returns: The introspection data for the interface. :rtype: :class:`dbus_next.introspection.Interface` """ # TODO cannot be overridden by a dbus member return intr.Interface(self.name, methods=[ method.introspection for method in ServiceInterface._get_methods(self) if not method.disabled ], signals=[ signal.introspection for signal in ServiceInterface._get_signals(self) if not signal.disabled ], properties=[ prop.introspection for prop in ServiceInterface._get_properties(self) if not prop.disabled ]) @staticmethod def _get_properties(interface): return interface.__properties @staticmethod def _get_methods(interface): return interface.__methods @staticmethod def _get_signals(interface): return interface.__signals @staticmethod def _get_buses(interface): return interface.__buses @staticmethod def _add_bus(interface, bus): interface.__buses.add(bus) @staticmethod def _remove_bus(interface, bus): interface.__buses.remove(bus) @staticmethod def _msg_body_to_args(msg): if signature_contains_type(msg.signature_tree, msg.body, 'h'): # XXX: This deep copy could be expensive if messages are very # large. We could optimize this by only copying what we change # here. return replace_idx_with_fds(msg.signature_tree, copy.deepcopy(msg.body), msg.unix_fds) else: return msg.body @staticmethod def _fn_result_to_body(result, signature_tree): '''The high level interfaces may return single values which may be wrapped in a list to be a message body. Also they may return fds directly for type 'h' which need to be put into an external list.''' out_len = len(signature_tree.types) if result is None: result = [] else: if out_len == 1: result = [result] else: if type(result) is not list: raise SignatureBodyMismatchError( 'Expected signal to return a list of arguments') if out_len != len(result): raise SignatureBodyMismatchError( f"Signature and function return mismatch, expected {len(signature_tree.types)} arguments but got {len(result)}" ) return replace_fds_with_idx(signature_tree, result) @staticmethod def _handle_signal(interface, signal, result): body, fds = ServiceInterface._fn_result_to_body(result, signal.signature_tree) for bus in ServiceInterface._get_buses(interface): bus._interface_signal_notify(interface, interface.name, signal.name, signal.signature, body, fds) @staticmethod def _get_property_value(interface, prop, callback): # XXX MUST CHECK TYPE RETURNED BY GETTER try: if asyncio.iscoroutinefunction(prop.prop_getter): task = asyncio.ensure_future(prop.prop_getter(interface)) def get_property_callback(task): try: result = task.result() except Exception as e: callback(interface, prop, None, e) return callback(interface, prop, result, None) task.add_done_callback(get_property_callback) return callback(interface, prop, getattr(interface, prop.prop_getter.__name__), None) except Exception as e: callback(interface, prop, None, e) @staticmethod def _set_property_value(interface, prop, value, callback): # XXX MUST CHECK TYPE TO SET try: if asyncio.iscoroutinefunction(prop.prop_setter): task = asyncio.ensure_future(prop.prop_setter(interface, value)) def set_property_callback(task): try: task.result() except Exception as e: callback(interface, prop, e) return callback(interface, prop, None) task.add_done_callback(set_property_callback) return setattr(interface, prop.prop_setter.__name__, value) callback(interface, prop, None) except Exception as e: callback(interface, prop, e) @staticmethod def _get_all_property_values(interface, callback, user_data=None): result = {} result_error = None for prop in ServiceInterface._get_properties(interface): if prop.disabled or not prop.access.readable(): continue result[prop.name] = None if not result: callback(interface, result, user_data, None) return def get_property_callback(interface, prop, value, e): nonlocal result_error if e is not None: result_error = e del result[prop.name] else: try: result[prop.name] = Variant(prop.signature, value) except SignatureBodyMismatchError as e: result_error = e del result[prop.name] if any(v is None for v in result.values()): return callback(interface, result, user_data, result_error) for prop in ServiceInterface._get_properties(interface): if prop.disabled or not prop.access.readable(): continue ServiceInterface._get_property_value(interface, prop, get_property_callback) python-dbus-next-0.2.3/dbus_next/signature.py000066400000000000000000000371351407735772700213460ustar00rootroot00000000000000from .validators import is_object_path_valid from .errors import InvalidSignatureError, SignatureBodyMismatchError from typing import Any, List, Union class SignatureType: """A class that represents a single complete type within a signature. This class is not meant to be constructed directly. Use the :class:`SignatureTree` class to parse signatures. :ivar ~.signature: The signature of this complete type. :vartype ~.signature: str :ivar children: A list of child types if this is a container type. Arrays \ have one child type, dict entries have two child types (key and value), and \ structs have child types equal to the number of struct members. :vartype children: list(:class:`SignatureType`) """ _tokens = 'ybnqiuxtdsogavh({' def __init__(self, token): self.token = token self.children = [] self._signature = None def __eq__(self, other): if type(other) is SignatureType: return self.signature == other.signature else: return super().__eq__(other) def _collapse(self): if self.token not in 'a({': return self.token signature = [self.token] for child in self.children: signature.append(child._collapse()) if self.token == '(': signature.append(')') elif self.token == '{': signature.append('}') return ''.join(signature) @property def signature(self) -> str: if self._signature is not None: return self._signature self._signature = self._collapse() return self._signature @staticmethod def _parse_next(signature): if not signature: return (None, '') token = signature[0] if token not in SignatureType._tokens: raise InvalidSignatureError(f'got unexpected token: "{token}"') # container types if token == 'a': self = SignatureType('a') (child, signature) = SignatureType._parse_next(signature[1:]) if not child: raise InvalidSignatureError('missing type for array') self.children.append(child) return (self, signature) elif token == '(': self = SignatureType('(') signature = signature[1:] while True: (child, signature) = SignatureType._parse_next(signature) if not signature: raise InvalidSignatureError('missing closing ")" for struct') self.children.append(child) if signature[0] == ')': return (self, signature[1:]) elif token == '{': self = SignatureType('{') signature = signature[1:] (key_child, signature) = SignatureType._parse_next(signature) if not key_child or len(key_child.children): raise InvalidSignatureError('expected a simple type for dict entry key') self.children.append(key_child) (value_child, signature) = SignatureType._parse_next(signature) if not value_child: raise InvalidSignatureError('expected a value for dict entry') if not signature or signature[0] != '}': raise InvalidSignatureError('missing closing "}" for dict entry') self.children.append(value_child) return (self, signature[1:]) # basic type return (SignatureType(token), signature[1:]) def _verify_byte(self, body): BYTE_MIN = 0x00 BYTE_MAX = 0xff if not isinstance(body, int): raise SignatureBodyMismatchError( f'DBus BYTE type "y" must be Python type "int", got {type(body)}') if body < BYTE_MIN or body > BYTE_MAX: raise SignatureBodyMismatchError( f'DBus BYTE type must be between {BYTE_MIN} and {BYTE_MAX}') def _verify_boolean(self, body): if not isinstance(body, bool): raise SignatureBodyMismatchError( f'DBus BOOLEAN type "b" must be Python type "bool", got {type(body)}') def _verify_int16(self, body): INT16_MIN = -0x7fff - 1 INT16_MAX = 0x7fff if not isinstance(body, int): raise SignatureBodyMismatchError( f'DBus INT16 type "n" must be Python type "int", got {type(body)}') elif body > INT16_MAX or body < INT16_MIN: raise SignatureBodyMismatchError( f'DBus INT16 type "n" must be between {INT16_MIN} and {INT16_MAX}') def _verify_uint16(self, body): UINT16_MIN = 0 UINT16_MAX = 0xffff if not isinstance(body, int): raise SignatureBodyMismatchError( f'DBus UINT16 type "q" must be Python type "int", got {type(body)}') elif body > UINT16_MAX or body < UINT16_MIN: raise SignatureBodyMismatchError( f'DBus UINT16 type "q" must be between {UINT16_MIN} and {UINT16_MAX}') def _verify_int32(self, body): INT32_MIN = -0x7fffffff - 1 INT32_MAX = 0x7fffffff if not isinstance(body, int): raise SignatureBodyMismatchError( f'DBus INT32 type "i" must be Python type "int", got {type(body)}') elif body > INT32_MAX or body < INT32_MIN: raise SignatureBodyMismatchError( f'DBus INT32 type "i" must be between {INT32_MIN} and {INT32_MAX}') def _verify_uint32(self, body): UINT32_MIN = 0 UINT32_MAX = 0xffffffff if not isinstance(body, int): raise SignatureBodyMismatchError( f'DBus UINT32 type "u" must be Python type "int", got {type(body)}') elif body > UINT32_MAX or body < UINT32_MIN: raise SignatureBodyMismatchError( f'DBus UINT32 type "u" must be between {UINT32_MIN} and {UINT32_MAX}') def _verify_int64(self, body): INT64_MAX = 9223372036854775807 INT64_MIN = -INT64_MAX - 1 if not isinstance(body, int): raise SignatureBodyMismatchError( f'DBus INT64 type "x" must be Python type "int", got {type(body)}') elif body > INT64_MAX or body < INT64_MIN: raise SignatureBodyMismatchError( f'DBus INT64 type "x" must be between {INT64_MIN} and {INT64_MAX}') def _verify_uint64(self, body): UINT64_MIN = 0 UINT64_MAX = 18446744073709551615 if not isinstance(body, int): raise SignatureBodyMismatchError( f'DBus UINT64 type "t" must be Python type "int", got {type(body)}') elif body > UINT64_MAX or body < UINT64_MIN: raise SignatureBodyMismatchError( f'DBus UINT64 type "t" must be between {UINT64_MIN} and {UINT64_MAX}') def _verify_double(self, body): if not isinstance(body, float) and not isinstance(body, int): raise SignatureBodyMismatchError( f'DBus DOUBLE type "d" must be Python type "float" or "int", got {type(body)}') def _verify_unix_fd(self, body): try: self._verify_uint32(body) except SignatureBodyMismatchError: raise SignatureBodyMismatchError('DBus UNIX_FD type "h" must be a valid UINT32') def _verify_object_path(self, body): if not is_object_path_valid(body): raise SignatureBodyMismatchError( 'DBus OBJECT_PATH type "o" must be a valid object path') def _verify_string(self, body): if not isinstance(body, str): raise SignatureBodyMismatchError( f'DBus STRING type "s" must be Python type "str", got {type(body)}') def _verify_signature(self, body): # I guess we could run it through the SignatureTree parser instead if not isinstance(body, str): raise SignatureBodyMismatchError( f'DBus SIGNATURE type "g" must be Python type "str", got {type(body)}') if len(body.encode()) > 0xff: raise SignatureBodyMismatchError('DBus SIGNATURE type "g" must be less than 256 bytes') def _verify_array(self, body): child_type = self.children[0] if child_type.token == '{': if not isinstance(body, dict): raise SignatureBodyMismatchError( f'DBus ARRAY type "a" with DICT_ENTRY child must be Python type "dict", got {type(body)}' ) for key, value in body.items(): child_type.children[0].verify(key) child_type.children[1].verify(value) elif child_type.token == 'y': if not isinstance(body, bytes): raise SignatureBodyMismatchError( f'DBus ARRAY type "a" with BYTE child must be Python type "bytes", got {type(body)}' ) # no need to verify children else: if not isinstance(body, list): raise SignatureBodyMismatchError( f'DBus ARRAY type "a" must be Python type "list", got {type(body)}') for member in body: child_type.verify(member) def _verify_struct(self, body): # TODO allow tuples if not isinstance(body, list): raise SignatureBodyMismatchError( f'DBus STRUCT type "(" must be Python type "list", got {type(body)}') if len(body) != len(self.children): raise SignatureBodyMismatchError( 'DBus STRUCT type "(" must have Python list members equal to the number of struct type members' ) for i, member in enumerate(body): self.children[i].verify(member) def _verify_variant(self, body): # a variant signature and value is valid by construction if not isinstance(body, Variant): raise SignatureBodyMismatchError( f'DBus VARIANT type "v" must be Python type "Variant", got {type(body)}') def verify(self, body: Any) -> bool: """Verify that the body matches this type. :returns: True if the body matches this type. :raises: :class:`SignatureBodyMismatchError` if the body does not match this type. """ if body is None: raise SignatureBodyMismatchError('Cannot serialize Python type "None"') elif self.token == 'y': self._verify_byte(body) elif self.token == 'b': self._verify_boolean(body) elif self.token == 'n': self._verify_int16(body) elif self.token == 'q': self._verify_uint16(body) elif self.token == 'i': self._verify_int32(body) elif self.token == 'u': self._verify_uint32(body) elif self.token == 'x': self._verify_int64(body) elif self.token == 't': self._verify_uint64(body) elif self.token == 'd': self._verify_double(body) elif self.token == 'h': self._verify_unix_fd(body) elif self.token == 'o': self._verify_object_path(body) elif self.token == 's': self._verify_string(body) elif self.token == 'g': self._verify_signature(body) elif self.token == 'a': self._verify_array(body) elif self.token == '(': self._verify_struct(body) elif self.token == 'v': self._verify_variant(body) else: raise Exception(f'cannot verify type with token {self.token}') return True class SignatureTree: """A class that represents a signature as a tree structure for conveniently working with DBus signatures. This class will not normally be used directly by the user. :ivar types: A list of parsed complete types. :vartype types: list(:class:`SignatureType`) :ivar ~.signature: The signature of this signature tree. :vartype ~.signature: str :raises: :class:`InvalidSignatureError` if the given signature is not valid. """ _cache = {} @staticmethod def _get(signature: str = ''): if signature in SignatureTree._cache: return SignatureTree._cache[signature] SignatureTree._cache[signature] = SignatureTree(signature) return SignatureTree._cache[signature] def __init__(self, signature: str = ''): self.signature = signature self.types = [] if len(signature) > 0xff: raise InvalidSignatureError('A signature must be less than 256 characters') while signature: (type_, signature) = SignatureType._parse_next(signature) self.types.append(type_) def __eq__(self, other): if type(other) is SignatureTree: return self.signature == other.signature else: return super().__eq__(other) def verify(self, body: List[Any]): """Verifies that the give body matches this signature tree :param body: the body to verify for this tree :type body: list(Any) :returns: True if the signature matches the body or an exception if not. :raises: :class:`SignatureBodyMismatchError` if the signature does not match the body. """ if not isinstance(body, list): raise SignatureBodyMismatchError(f'The body must be a list (got {type(body)})') if len(body) != len(self.types): raise SignatureBodyMismatchError( f'The body has the wrong number of types (got {len(body)}, expected {len(self.types)})' ) for i, type_ in enumerate(self.types): type_.verify(body[i]) return True class Variant: """A class to represent a DBus variant (type "v"). This class is used in message bodies to represent variants. The user can expect a value in the body with type "v" to use this class and can construct this class directly for use in message bodies sent over the bus. :ivar signature: The signature for this variant. Must be a single complete type. :vartype signature: str :ivar signature_type: The parsed signature of this variant. :vartype signature_type: :class:`SignatureType` :ivar value: The value of this variant. Must correspond to the signature. :vartype value: Any :raises: :class:`InvalidSignatureError` if the signature is not valid. :class:`SignatureBodyMismatchError` if the signature does not match the body. """ def __init__(self, signature: Union[str, SignatureTree, SignatureType], value: Any): signature_str = '' signature_tree = None signature_type = None if type(signature) is SignatureTree: signature_tree = signature elif type(signature) is SignatureType: signature_type = signature signature_str = signature.signature elif type(signature) is str: signature_tree = SignatureTree._get(signature) else: raise TypeError('signature must be a SignatureTree, SignatureType, or a string') if signature_tree: if len(signature_tree.types) != 1: raise ValueError('variants must have a signature for a single complete type') signature_str = signature_tree.signature signature_type = signature_tree.types[0] signature_type.verify(value) self.type = signature_type self.signature = signature_str self.value = value def __eq__(self, other): if type(other) is Variant: return self.signature == other.signature and self.value == other.value else: return super().__eq__(other) def __repr__(self): return "" % (self.type.signature, self.value) python-dbus-next-0.2.3/dbus_next/validators.py000066400000000000000000000112651407735772700215110ustar00rootroot00000000000000import re from .errors import InvalidBusNameError, InvalidObjectPathError, InvalidInterfaceNameError, InvalidMemberNameError _bus_name_re = re.compile(r'^[A-Za-z_-][A-Za-z0-9_-]*$') _path_re = re.compile(r'^[A-Za-z0-9_]+$') _element_re = re.compile(r'^[A-Za-z_][A-Za-z0-9_]*$') def is_bus_name_valid(name: str) -> bool: """Whether this is a valid bus name. .. seealso:: https://dbus.freedesktop.org/doc/dbus-specification.html#message-protocol-names-bus :param name: The bus name to validate. :type name: str :returns: Whether the name is a valid bus name. :rtype: bool """ if not isinstance(name, str): return False if not name or len(name) > 255: return False if name.startswith(':'): # a unique bus name return True if name.startswith('.'): return False if name.find('.') == -1: return False for element in name.split('.'): if _bus_name_re.search(element) is None: return False return True def is_object_path_valid(path: str) -> bool: """Whether this is a valid object path. .. seealso:: https://dbus.freedesktop.org/doc/dbus-specification.html#message-protocol-marshaling-object-path :param path: The object path to validate. :type path: str :returns: Whether the object path is valid. :rtype: bool """ if not isinstance(path, str): return False if not path: return False if not path.startswith('/'): return False if len(path) == 1: return True for element in path[1:].split('/'): if _path_re.search(element) is None: return False return True def is_interface_name_valid(name: str) -> bool: """Whether this is a valid interface name. .. seealso:: https://dbus.freedesktop.org/doc/dbus-specification.html#message-protocol-names-interface :param name: The interface name to validate. :type name: str :returns: Whether the name is a valid interface name. :rtype: bool """ if not isinstance(name, str): return False if not name or len(name) > 255: return False if name.startswith('.'): return False if name.find('.') == -1: return False for element in name.split('.'): if _element_re.search(element) is None: return False return True def is_member_name_valid(member: str) -> bool: """Whether this is a valid member name. .. seealso:: https://dbus.freedesktop.org/doc/dbus-specification.html#message-protocol-names-member :param member: The member name to validate. :type member: str :returns: Whether the name is a valid member name. :rtype: bool """ if not isinstance(member, str): return False if not member or len(member) > 255: return False if _element_re.search(member) is None: return False return True def assert_bus_name_valid(name: str): """Raise an error if this is not a valid bus name. .. seealso:: https://dbus.freedesktop.org/doc/dbus-specification.html#message-protocol-names-bus :param name: The bus name to validate. :type name: str :raises: - :class:`InvalidBusNameError` - If this is not a valid bus name. """ if not is_bus_name_valid(name): raise InvalidBusNameError(name) def assert_object_path_valid(path: str): """Raise an error if this is not a valid object path. .. seealso:: https://dbus.freedesktop.org/doc/dbus-specification.html#message-protocol-marshaling-object-path :param path: The object path to validate. :type path: str :raises: - :class:`InvalidObjectPathError` - If this is not a valid object path. """ if not is_object_path_valid(path): raise InvalidObjectPathError(path) def assert_interface_name_valid(name: str): """Raise an error if this is not a valid interface name. .. seealso:: https://dbus.freedesktop.org/doc/dbus-specification.html#message-protocol-names-interface :param name: The interface name to validate. :type name: str :raises: - :class:`InvalidInterfaceNameError` - If this is not a valid object path. """ if not is_interface_name_valid(name): raise InvalidInterfaceNameError(name) def assert_member_name_valid(member): """Raise an error if this is not a valid member name. .. seealso:: https://dbus.freedesktop.org/doc/dbus-specification.html#message-protocol-names-member :param member: The member name to validate. :type member: str :raises: - :class:`InvalidMemberNameError` - If this is not a valid object path. """ if not is_member_name_valid(member): raise InvalidMemberNameError(member) python-dbus-next-0.2.3/docs/000077500000000000000000000000001407735772700157175ustar00rootroot00000000000000python-dbus-next-0.2.3/docs/Makefile000066400000000000000000000011361407735772700173600ustar00rootroot00000000000000# Minimal makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build SPHINXPROJ = dbus-next SOURCEDIR = . BUILDDIR = _build # Put it first so that "make" without argument is like "make help". help: @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) .PHONY: help Makefile # Catch-all target: route all unknown targets to Sphinx using the new # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). %: Makefile @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)python-dbus-next-0.2.3/docs/_static/000077500000000000000000000000001407735772700173455ustar00rootroot00000000000000python-dbus-next-0.2.3/docs/_static/.gitignore000066400000000000000000000000001407735772700213230ustar00rootroot00000000000000python-dbus-next-0.2.3/docs/_templates/000077500000000000000000000000001407735772700200545ustar00rootroot00000000000000python-dbus-next-0.2.3/docs/_templates/.gitignore000066400000000000000000000000001407735772700220320ustar00rootroot00000000000000python-dbus-next-0.2.3/docs/authentication.rst000066400000000000000000000005661407735772700214770ustar00rootroot00000000000000Authentication ============== Classes for the DBus `authentication protocol `_ for us with :class:`MessageBus ` implementations. .. autoclass:: dbus_next.auth.Authenticator .. autoclass:: dbus_next.auth.AuthExternal .. autoclass:: dbus_next.auth.AuthAnnonymous python-dbus-next-0.2.3/docs/conf.py000066400000000000000000000116771407735772700172320ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # Configuration file for the Sphinx documentation builder. # # This file does only contain a selection of the most common options. For a # full list see the documentation: # http://www.sphinx-doc.org/en/master/config # -- Path setup -------------------------------------------------------------- # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # import os import sys sys.path.insert(0, os.path.abspath(os.path.dirname(__file__) + '/..')) from dbus_next.__version__ import __title__, __author__, __version__, __copyright__ _project_slug = __title__.replace('_', '-') # -- Project information ----------------------------------------------------- project = _project_slug copyright = __copyright__ author = __author__ # The short X.Y version version = __version__ # The full version, including alpha/beta/rc tags release = __version__ # -- General configuration --------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. # # needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.githubpages', 'sphinxcontrib.asyncio', 'sphinxcontrib.fulltoc' ] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # # source_suffix = ['.rst', '.md'] source_suffix = '.rst' # The master toctree document. master_doc = 'index' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. language = None # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This pattern also affects html_static_path and html_extra_path . exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # -- Options for HTML output ------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # html_theme = 'alabaster' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. # # html_theme_options = {} # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # Custom sidebar templates, must be a dictionary that maps document names # to template names. # # The default sidebars (for documents that don't match any pattern) are # defined by theme itself. Builtin themes are using these templates by # default: ``['localtoc.html', 'relations.html', 'sourcelink.html', # 'searchbox.html']``. # # html_sidebars = {} # -- Options for HTMLHelp output --------------------------------------------- # Output file base name for HTML help builder. htmlhelp_basename = 'dbus-nextdoc' # -- Options for LaTeX output ------------------------------------------------ latex_elements = { # The paper size ('letterpaper' or 'a4paper'). # # 'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). # # 'pointsize': '10pt', # Additional stuff for the LaTeX preamble. # # 'preamble': '', # Latex figure (float) alignment # # 'figure_align': 'htbp', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ (master_doc, 'dbus-next.tex', 'dbus-next Documentation', __author__, 'manual'), ] # -- Options for manual page output ------------------------------------------ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [(master_doc, _project_slug, 'dbus-next Documentation', [author], 1)] # -- Options for Texinfo output ---------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ (master_doc, _project_slug, 'dbus-next Documentation', author, _project_slug, 'One line description of project.', 'Miscellaneous'), ] # -- Extension configuration ------------------------------------------------- python-dbus-next-0.2.3/docs/constants.rst000066400000000000000000000012421407735772700204640ustar00rootroot00000000000000Constants ========= .. autoclass:: dbus_next.BusType :members: :undoc-members: .. autoclass:: dbus_next.MessageType :members: :undoc-members: .. autoclass:: dbus_next.MessageFlag :members: :undoc-members: .. autoclass:: dbus_next.NameFlag :members: :undoc-members: .. autoclass:: dbus_next.RequestNameReply :members: :undoc-members: .. autoclass:: dbus_next.ReleaseNameReply :members: :undoc-members: .. autoclass:: dbus_next.PropertyAccess :members: :undoc-members: .. autoclass:: dbus_next.ArgDirection :members: :undoc-members: .. autoclass:: dbus_next.ErrorType :members: :undoc-members: python-dbus-next-0.2.3/docs/errors.rst000066400000000000000000000012051407735772700177630ustar00rootroot00000000000000Errors ====== .. autoclass:: dbus_next.DBusError :members: :undoc-members: .. autoclass:: dbus_next.SignatureBodyMismatchError .. autoclass:: dbus_next.InvalidSignatureError .. autoclass:: dbus_next.InvalidAddressError .. autoclass:: dbus_next.AuthError .. autoclass:: dbus_next.InvalidMessageError .. autoclass:: dbus_next.InvalidIntrospectionError .. autoclass:: dbus_next.InterfaceNotFoundError .. autoclass:: dbus_next.SignalDisabledError .. autoclass:: dbus_next.InvalidBusNameError .. autoclass:: dbus_next.InvalidObjectPathError .. autoclass:: dbus_next.InvalidInterfaceNameError .. autoclass:: dbus_next.InvalidMemberNameError python-dbus-next-0.2.3/docs/high-level-client/000077500000000000000000000000001407735772700212175ustar00rootroot00000000000000python-dbus-next-0.2.3/docs/high-level-client/aio-proxy-interface.rst000066400000000000000000000002141407735772700256330ustar00rootroot00000000000000aio.ProxyInterface ================== .. autoclass:: dbus_next.aio.ProxyInterface :members: :undoc-members: :show-inheritance: python-dbus-next-0.2.3/docs/high-level-client/aio-proxy-object.rst000066400000000000000000000002031407735772700251370ustar00rootroot00000000000000aio.ProxyObject =============== .. autoclass:: dbus_next.aio.ProxyObject :members: :undoc-members: :show-inheritance: python-dbus-next-0.2.3/docs/high-level-client/base-proxy-interface.rst000066400000000000000000000002021407735772700257720ustar00rootroot00000000000000BaseProxyInterface ================== .. autoclass:: dbus_next.proxy_object.BaseProxyInterface :members: :undoc-members: python-dbus-next-0.2.3/docs/high-level-client/base-proxy-object.rst000066400000000000000000000001711407735772700253050ustar00rootroot00000000000000BaseProxyObject =============== .. autoclass:: dbus_next.proxy_object.BaseProxyObject :members: :undoc-members: python-dbus-next-0.2.3/docs/high-level-client/glib-proxy-interface.rst000066400000000000000000000002171407735772700260030ustar00rootroot00000000000000glib.ProxyInterface =================== .. autoclass:: dbus_next.glib.ProxyInterface :members: :undoc-members: :show-inheritance: python-dbus-next-0.2.3/docs/high-level-client/glib-proxy-object.rst000066400000000000000000000002061407735772700253070ustar00rootroot00000000000000glib.ProxyObject ================ .. autoclass:: dbus_next.glib.ProxyObject :members: :undoc-members: :show-inheritance: python-dbus-next-0.2.3/docs/high-level-client/index.rst000066400000000000000000000117531407735772700230670ustar00rootroot00000000000000The High Level Client ===================== .. toctree:: :maxdepth: 2 base-proxy-object base-proxy-interface aio-proxy-object aio-proxy-interface glib-proxy-object glib-proxy-interface 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: .. code-block:: xml 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. 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. 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 `. 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. :example: .. code-block:: python3 from dbus_next.aio import MessageBus from dbus_next import Variant bus = await MessageBus().connect() with open('introspection.xml', 'r') as f: introspection = f.read() # alternatively, get the data dynamically: # introspection = await bus.introspect('com.example.name', # '/com/example/sample_object0') proxy_object = bus.get_proxy_object('com.example.name', '/com/example/sample_object0', introspection) interface = proxy_object.get_interface('com.example.SampleInterface0') # Use call_[METHOD] in snake case to call methods, passing the # in args and receiving the out args. The `baz` returned will # be type 'a{us}' which translates to a Python dict with `int` # keys and `str` values. baz = await interface.call_frobate(5, 'hello') # `bar` will be a Variant. bar = await interface.call_bazify([-5, 5, 5]) await interface.call_mogrify([5, 5, [ Variant('s', 'foo') ]) # Listen to signals by defining a callback that takes the args # specified by the signal definition and registering it on the # interface with on_[SIGNAL] in snake case. def changed_notify(new_value): print(f'The new value is: {new_value}') interface.on_changed(changed_notify) # Use get_[PROPERTY] and set_[PROPERTY] with the property in # snake case to get and set the property. bar_value = await interface.get_bar() await interface.set_bar(105) await bus.wait_for_disconnect() python-dbus-next-0.2.3/docs/high-level-service/000077500000000000000000000000001407735772700214015ustar00rootroot00000000000000python-dbus-next-0.2.3/docs/high-level-service/index.rst000066400000000000000000000127421407735772700232500ustar00rootroot00000000000000The High Level Service ====================== .. toctree:: :maxdepth: 2 service-interface 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. 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. 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. 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. 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. 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``. 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. 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. 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. :example: .. code-block:: python3 from dbus_next.aio import MessageBus from dbus_next.service import (ServiceInterface, method, dbus_property, signal) from dbus_next import Variant, DBusError import asyncio class ExampleInterface(ServiceInterface): def __init__(self): super().__init__('com.example.SampleInterface0') self._bar = 105 @method() def Frobate(self, foo: 'i', bar: 's') -> 'a{us}': print(f'called Frobate with foo={foo} and bar={bar}') return { 1: 'one', 2: 'two' } @method() async def Bazify(self, bar: '(iiu)') -> 'vv': print(f'called Bazify with bar={bar}') return [Variant('s', 'example'), Variant('s', 'bazify')] @method() def Mogrify(self, bar: '(iiav)'): raise DBusError('com.example.error.CannotMogrify', 'it is not possible to mogrify') @signal() def Changed(self) -> 'b': return True @dbus_property() def Bar(self) -> 'y': return self._bar @Bar.setter def Bar(self, val: 'y'): if self._bar == val: return self._bar = val self.emit_properties_changed({'Bar': self._bar}) async def main(): bus = await MessageBus().connect() interface = ExampleInterface() bus.export('/com/example/sample0', interface) await bus.request_name('com.example.name') # emit the changed signal after two seconds. await asyncio.sleep(2) interface.changed() await bus.wait_for_disconnect() asyncio.get_event_loop().run_until_complete(main()) python-dbus-next-0.2.3/docs/high-level-service/service-interface.rst000066400000000000000000000004051407735772700255300ustar00rootroot00000000000000ServiceInterface ================ .. autoclass:: dbus_next.service.ServiceInterface :members: :undoc-members: .. autodecorator:: dbus_next.service.dbus_property .. autodecorator:: dbus_next.service.method .. autodecorator:: dbus_next.service.signal python-dbus-next-0.2.3/docs/index.rst000066400000000000000000000051361407735772700175650ustar00rootroot00000000000000Python DBus-Next Documentation ============================== .. module:: dbus_next .. toctree:: :maxdepth: 3 :caption: Reference: type-system/index.rst high-level-client/index.rst high-level-service/index.rst low-level-interface/index.rst message-bus/index.rst introspection validators constants errors authentication Overview ++++++++ Python DBus-Next is a library for the `DBus message bus system `_ for interprocess communcation in a Linux desktop or mobile environment. 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. Desktop users can use this library to create their own scripts and utilities to interact with those interfaces for customization of their desktop environment. While other libraries for DBus exist for Python, this library offers the following improvements: - Zero dependencies and pure Python 3. - Support for multiple main loop backends including asyncio and the GLib main loop. - Nonblocking IO suitable for GUI development. - Target the latest language features of Python for beautiful services and clients. - Complete implementation of the DBus type system without ever guessing types. - Integration tests for all features of the library. - Completely documented public API. The library offers three core interfaces: - `The High Level Client `_ - Communicate with an existing interface exported on the bus by another client through a proxy object. - `The High Level Service `_ - Export a service interface for your application other clients can connect to for interaction with your application at runtime. - `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. Installation ++++++++++++ This library is available on PyPi as `dbus-next `_. .. code-block:: bash pip3 install dbus-next Contributing ++++++++++++ Development for this library happens on `Github `_. Report bugs or request features there. Contributions are welcome. License ++++++++ This library is available under an `MIT License `_. © 2019, Tony Crisci Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` python-dbus-next-0.2.3/docs/introspection.rst000066400000000000000000000010021407735772700213420ustar00rootroot00000000000000Introspection ============= .. autoclass:: dbus_next.introspection.Node :members: :undoc-members: .. autoclass:: dbus_next.introspection.Interface :members: :undoc-members: .. autoclass:: dbus_next.introspection.Property :members: :undoc-members: .. autoclass:: dbus_next.introspection.Method :members: :undoc-members: .. autoclass:: dbus_next.introspection.Signal :members: :undoc-members: .. autoclass:: dbus_next.introspection.Arg :members: :undoc-members: python-dbus-next-0.2.3/docs/low-level-interface/000077500000000000000000000000001407735772700215635ustar00rootroot00000000000000python-dbus-next-0.2.3/docs/low-level-interface/index.rst000066400000000000000000000074301407735772700234300ustar00rootroot00000000000000The Low Level Interface ======================= .. toctree:: :maxdepth: 2 message 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: - Implementing an application that works with DBus directly like ``dbus-send(1)`` or ``dbus-monitor(1)``. - Creating a new implementation of the :class:`BaseMessageBus `. - Creating clients or services that use an alternative to the standard DBus interfaces. The primary methods and classes of the low-level interface are: - :class:`Message ` - :func:`MessageBus.send() ` - :func:`MessageBus.add_message_handler() ` - :func:`MessageBus.remove_message_handler() ` - :func:`MessageBus.next_serial() ` - :func:`aio.MessageBus.call() ` - :func:`glib.MessageBus.call() ` - :func:`glib.MessageBus.call_sync() ` Mixed use of the low and high level interfaces on the same bus connection is not recommended. :example: Call a standard interface .. code-block:: python3 bus = await MessageBus().connect() msg = Message(destination='org.freedesktop.DBus', path='/org/freedesktop/DBus', interface='org.freedesktop.DBus', member='ListNames', serial=bus.next_serial()) reply = await bus.call(msg) assert reply.message_type == MessageType.METHOD_RETURN print(reply.body[0]) :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. .. code-block:: python3 bus = await MessageBus().connect() reply = await bus.call( Message(destination='org.freedesktop.DBus', path='/org/freedesktop/DBus', member='AddMatch', signature='s', body=["member='MyMember', interface='com.test.interface'"])) assert reply.message_type == MessageType.METHOD_RETURN def message_handler(msg): if msg.interface == 'com.test.interface' and msg.member == 'MyMember': return Message.new_method_return(msg, 's', ['got it']) bus.add_message_handler(message_handler) await bus.wait_for_disconnect() :example: Emit a signal .. code-block:: python3 bus = await MessageBus().connect() await bus.send(Message.new_signal('/com/test/path', 'com.test.interface', 'SomeSignal', 's', ['a signal'])) :example: Send a file descriptor. The message format will be the same when received on the client side. You are responsible for closing any file descriptor that is 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. .. code-block:: python3 bus = await MessageBus().connect(negotiate_unix_fd=True) fd = os.open('/dev/null', os.O_RDONLY) msg = Message(destination='org.test.destination', path='/org/test/destination', interface='org.test.interface', member='TestMember', signature='h', body=[0], unix_fds=[fd]) await bus.send(msg) python-dbus-next-0.2.3/docs/low-level-interface/message.rst000066400000000000000000000001241407735772700237360ustar00rootroot00000000000000Message ======= .. autoclass:: dbus_next.Message :members: :undoc-members: python-dbus-next-0.2.3/docs/message-bus/000077500000000000000000000000001407735772700201325ustar00rootroot00000000000000python-dbus-next-0.2.3/docs/message-bus/aio-message-bus.rst000066400000000000000000000002041407735772700236410ustar00rootroot00000000000000aio.MessageBus ============== .. autoclass:: dbus_next.aio.MessageBus :members: :inherited-members: :show-inheritance: python-dbus-next-0.2.3/docs/message-bus/base-message-bus.rst000066400000000000000000000001411407735772700240030ustar00rootroot00000000000000BaseMessageBus ============== .. autoclass:: dbus_next.message_bus.BaseMessageBus :members: python-dbus-next-0.2.3/docs/message-bus/glib-message-bus.rst000066400000000000000000000002071407735772700240110ustar00rootroot00000000000000glib.MessageBus =============== .. autoclass:: dbus_next.glib.MessageBus :members: :inherited-members: :show-inheritance: python-dbus-next-0.2.3/docs/message-bus/index.rst000066400000000000000000000020721407735772700217740ustar00rootroot00000000000000The Message Bus =============== .. toctree:: :maxdepth: 2 base-message-bus.rst aio-message-bus.rst glib-message-bus.rst 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. 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. 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. For more information on how to use the message bus, see the documentation for the specific interfaces you plan to use. python-dbus-next-0.2.3/docs/type-system/000077500000000000000000000000001407735772700202225ustar00rootroot00000000000000python-dbus-next-0.2.3/docs/type-system/index.rst000066400000000000000000000233071407735772700220700ustar00rootroot00000000000000The Type System =============== .. toctree:: :maxdepth: 2 variant signature-tree signature-type Values that are sent or received over the message bus always have an associated signature that specifies the types of those values. For the high-level client and service, these signatures are specified in XML data which is advertised in a `standard DBus interface `__. The high-level client dynamically creates classes based on this introspection data with methods and signals with arguments based on the type signature. The high-level service does the inverse by introspecting the class to create the introspection XML data which is advertised on the bus for clients. Each token in the signature is mapped to a Python type as shown in the table below. +-------------+-------+--------------------------------------+-------------------------------------------------------------------------+ | Name | Token | Python | Notes | | | | Type | | +-------------+-------+--------------------------------------+-------------------------------------------------------------------------+ | BYTE | y | int | An integer 0-255. In an array, it has type ``bytes``. | +-------------+-------+--------------------------------------+-------------------------------------------------------------------------+ | BOOLEAN | b | bool | | +-------------+-------+--------------------------------------+-------------------------------------------------------------------------+ | INT16 | n | int | | +-------------+-------+--------------------------------------+-------------------------------------------------------------------------+ | UINT16 | q | int | | +-------------+-------+--------------------------------------+-------------------------------------------------------------------------+ | INT32 | i | int | | +-------------+-------+--------------------------------------+-------------------------------------------------------------------------+ | UINT32 | u | int | | +-------------+-------+--------------------------------------+-------------------------------------------------------------------------+ | INT64 | x | int | | +-------------+-------+--------------------------------------+-------------------------------------------------------------------------+ | UINT64 | t | int | | +-------------+-------+--------------------------------------+-------------------------------------------------------------------------+ | DOUBLE | d | float | | +-------------+-------+--------------------------------------+-------------------------------------------------------------------------+ | STRING | s | str | | +-------------+-------+--------------------------------------+-------------------------------------------------------------------------+ | OBJECT_PATH | o | str | Must be a valid object path. | +-------------+-------+--------------------------------------+-------------------------------------------------------------------------+ | SIGNATURE | g | str | Must be a valid signature. | +-------------+-------+--------------------------------------+-------------------------------------------------------------------------+ | UNIX_FD | h | int | In the low-level interface, an index pointing to a file descriptor | | | | | in the ``unix_fds`` member of the :class:`Message `. | | | | | In the high-level interface, it is the file descriptor itself. | +-------------+-------+--------------------------------------+-------------------------------------------------------------------------+ | ARRAY | a | list | Must be followed by a complete type which specifies the child type. | +-------------+-------+--------------------------------------+-------------------------------------------------------------------------+ | STRUCT | ( | list | Types in the Python ``list`` must match the types between the parens. | +-------------+-------+--------------------------------------+-------------------------------------------------------------------------+ | VARIANT | v | :class:`Variant ` | This class is provided by the library. | | | | | | +-------------+-------+--------------------------------------+-------------------------------------------------------------------------+ | DICT_ENTRY | { | dict | Must be included in an array to be a ``dict``. | +-------------+-------+--------------------------------------+-------------------------------------------------------------------------+ The types ``a``, ``(``, ``v``, and ``{`` are container types that hold other values. Examples of container types and Python examples are in the table below. +-----------+--------------------------------------+-------------------------------------------------------+ | Signature | Example | Notes | +===========+======================================+=======================================================+ | ``(su)`` | ``[ 'foo', 5 ]`` | Each element in the array must match the | | | | corresponding type of the struct member. | +-----------+--------------------------------------+-------------------------------------------------------+ | ``as`` | ``[ 'foo', 'bar' ]`` | The child type comes immediately after the ``a``. | | | | The array can have any number of elements, but | | | | they all must match the child type. | +-----------+--------------------------------------+-------------------------------------------------------+ | ``a{su}`` | ``{ 'foo': 5 }`` | An "array of dict entries" is represented by a | | | | ``dict``. The type after ``{`` is the key type and | | | | the type before the ``}`` is the value type. | +-----------+--------------------------------------+-------------------------------------------------------+ | ``ay`` | ``b'\0x62\0x75\0x66'`` | Special case: an array of bytes is represented by | | | | Python ``bytes``. | | | | | | | | | | | | | | | | | +-----------+--------------------------------------+-------------------------------------------------------+ | ``v`` | ``Variant('as', ['hello'])`` | Signature must be a single type. A variant may hold a | | | | container type. | | | | | | | | | | | | | +-----------+--------------------------------------+-------------------------------------------------------+ | ``(asv)`` | ``[ ['foo'], Variant('s', 'bar') ]`` | Containers may be nested. | +-----------+--------------------------------------+-------------------------------------------------------+ For more information on the DBus type system, see `the specification `__. python-dbus-next-0.2.3/docs/type-system/signature-tree.rst000066400000000000000000000001471407735772700237140ustar00rootroot00000000000000SignatureTree ============= .. autoclass:: dbus_next.SignatureTree :members: :undoc-members: python-dbus-next-0.2.3/docs/type-system/signature-type.rst000066400000000000000000000002101407735772700237250ustar00rootroot00000000000000SignatureType ============== .. autoclass:: dbus_next.SignatureType :members: :undoc-members: :exclude-members: signature python-dbus-next-0.2.3/docs/type-system/variant.rst000066400000000000000000000001241407735772700224150ustar00rootroot00000000000000Variant ======= .. autoclass:: dbus_next.Variant :members: :undoc-members: python-dbus-next-0.2.3/docs/validators.rst000066400000000000000000000006571407735772700206310ustar00rootroot00000000000000Validators ========== .. autofunction:: dbus_next.is_bus_name_valid .. autofunction:: dbus_next.is_member_name_valid .. autofunction:: dbus_next.is_object_path_valid .. autofunction:: dbus_next.is_interface_name_valid .. autofunction:: dbus_next.assert_bus_name_valid .. autofunction:: dbus_next.assert_member_name_valid .. autofunction:: dbus_next.assert_object_path_valid .. autofunction:: dbus_next.assert_interface_name_valid python-dbus-next-0.2.3/examples/000077500000000000000000000000001407735772700166055ustar00rootroot00000000000000python-dbus-next-0.2.3/examples/aio-list-names.py000077500000000000000000000012721407735772700220060ustar00rootroot00000000000000#!/usr/bin/env python3 import sys import os sys.path.append(os.path.abspath(os.path.dirname(__file__) + '/..')) from dbus_next import Message, MessageType from dbus_next.aio import MessageBus import asyncio import json loop = asyncio.get_event_loop() async def main(): bus = await MessageBus().connect() reply = await bus.call( Message(destination='org.freedesktop.DBus', path='/org/freedesktop/DBus', interface='org.freedesktop.DBus', member='ListNames')) if reply.message_type == MessageType.ERROR: raise Exception(reply.body[0]) print(json.dumps(reply.body[0], indent=2)) loop.run_until_complete(main()) python-dbus-next-0.2.3/examples/aio-tcp-notification.py000077500000000000000000000024701407735772700232050ustar00rootroot00000000000000#!/usr/bin/env python3 # In order for this to work a local tcp connection to the DBus a port # must be opened to forward to the dbus socket file. The easiest way # to achieve this is using "socat": # socat TCP-LISTEN:55556,reuseaddr,fork,range=127.0.0.1/32 UNIX-CONNECT:$(echo $DBUS_SESSION_BUS_ADDRESS | sed 's/unix:path=//g') # For actual DBus transport over network the authentication might # be a further problem. More information here: # https://dbus.freedesktop.org/doc/dbus-specification.html#auth-mechanisms import sys import os sys.path.append(os.path.abspath(os.path.dirname(__file__) + '/..')) from dbus_next.aio import MessageBus import asyncio loop = asyncio.get_event_loop() async def main(): bus = await MessageBus(bus_address="tcp:host=127.0.0.1,port=55556").connect() introspection = await bus.introspect('org.freedesktop.Notifications', '/org/freedesktop/Notifications') obj = bus.get_proxy_object('org.freedesktop.Notifications', '/org/freedesktop/Notifications', introspection) notification = obj.get_interface('org.freedesktop.Notifications') await notification.call_notify("test.py", 0, "", "DBus Test", "Test notification", [""], dict(), 5000) loop.run_until_complete(main()) python-dbus-next-0.2.3/examples/dbus-next-send.py000077500000000000000000000072251407735772700220300ustar00rootroot00000000000000#!/usr/bin/env python3 import sys import os sys.path.append(os.path.abspath(os.path.dirname(__file__) + '/..')) from dbus_next.validators import (is_bus_name_valid, is_member_name_valid, is_object_path_valid, is_interface_name_valid) from dbus_next.aio import MessageBus from dbus_next import MessageType, BusType, Message, Variant from argparse import ArgumentParser, OPTIONAL import json import asyncio parser = ArgumentParser() parser.add_argument('--system', help='Use the system bus', action='store_true') parser.add_argument('--session', help='Use the session bus', action='store_true') parser.add_argument('--dest', help='The destination address for the message', required=True) parser.add_argument('--signature', help='The signature for the message body') parser.add_argument('--type', help='The type of message to send', choices=[e.name for e in MessageType], default=MessageType.METHOD_CALL.name, nargs=OPTIONAL) parser.add_argument('object_path', help='The object path for the message') parser.add_argument('interface.member', help='The interface and member for the message') parser.add_argument('body', help='The JSON encoded body of the message. Must match the signature', nargs=OPTIONAL) args = parser.parse_args() def exit_error(message): parser.print_usage() print() print(message) sys.exit(1) interface_member = vars(args)['interface.member'].split('.') if len(interface_member) < 2: exit_error( f'Expecting an interface and member separated by a dot: {vars(args)["interface.member"]}') destination = args.dest member = interface_member[-1] interface = '.'.join(interface_member[:len(interface_member) - 1]) object_path = args.object_path signature = args.signature body = args.body message_type = MessageType[args.type] signature = args.signature bus_type = BusType.SESSION if args.system: bus_type = BusType.SYSTEM if message_type is not MessageType.METHOD_CALL: exit_error('only message type METHOD_CALL is supported right now') if not is_bus_name_valid(destination): exit_error(f'got invalid bus name: {destination}') if not is_object_path_valid(object_path): exit_error(f'got invalid object path: {object_path}') if not is_interface_name_valid(interface): exit_error(f'got invalid interface name: {interface}') if not is_member_name_valid(member): exit_error(f'got invalid member name: {member}') if body is None: body = [] signature = '' else: try: body = json.loads(body) except json.JSONDecodeError as e: exit_error(f'could not parse body as JSON: ({e})') if type(body) is not list: exit_error('body must be an array of arguments') if not signature: exit_error('--signature is a required argument when passing a message body') loop = asyncio.get_event_loop() async def main(): bus = await MessageBus(bus_type=bus_type).connect() message = Message(destination=destination, member=member, interface=interface, path=object_path, signature=signature, body=body) result = await bus.call(message) ret = 0 if result.message_type is MessageType.ERROR: print(f'Error: {result.error_name}', file=sys.stderr) ret = 1 def default(o): if type(o) is Variant: return [o.signature, o.value] else: raise json.JSONDecodeError() print(json.dumps(result.body, indent=2, default=default)) sys.exit(ret) loop.run_until_complete(main()) python-dbus-next-0.2.3/examples/example-service.py000077500000000000000000000032001407735772700222460ustar00rootroot00000000000000#!/usr/bin/env python3 import sys import os sys.path.append(os.path.abspath(os.path.dirname(__file__) + '/..')) from dbus_next.service import ServiceInterface, method, signal, dbus_property from dbus_next.aio.message_bus import MessageBus from dbus_next import Variant import asyncio class ExampleInterface(ServiceInterface): def __init__(self, name): super().__init__(name) self._string_prop = 'kevin' @method() def Echo(self, what: 's') -> 's': return what @method() def EchoMultiple(self, what1: 's', what2: 's') -> 'ss': return [what1, what2] @method() def GetVariantDict(self) -> 'a{sv}': return { 'foo': Variant('s', 'bar'), 'bat': Variant('x', -55), 'a_list': Variant('as', ['hello', 'world']) } @dbus_property(name='StringProp') def string_prop(self) -> 's': return self._string_prop @string_prop.setter def string_prop_setter(self, val: 's'): self._string_prop = val @signal() def signal_simple(self) -> 's': return 'hello' @signal() def signal_multiple(self) -> 'ss': return ['hello', 'world'] async def main(): name = 'dbus.next.example.service' path = '/example/path' interface_name = 'example.interface' bus = await MessageBus().connect() interface = ExampleInterface(interface_name) bus.export('/example/path', interface) await bus.request_name(name) print(f'service up on name: "{name}", path: "{path}", interface: "{interface_name}"') await bus.wait_for_disconnect() asyncio.get_event_loop().run_until_complete(main()) python-dbus-next-0.2.3/examples/glib-list-names.py000077500000000000000000000011451407735772700221520ustar00rootroot00000000000000#!/usr/bin/env python3 import sys import os sys.path.append(os.path.abspath(os.path.dirname(__file__) + '/..')) from dbus_next import Message from dbus_next.glib import MessageBus import json import signal from gi.repository import GLib main = GLib.MainLoop() bus = MessageBus().connect_sync() def reply_handler(reply, err): main.quit() if err: raise err print(json.dumps(reply.body[0], indent=2)) bus.call( Message('org.freedesktop.DBus', '/org/freedesktop/DBus', 'org.freedesktop.DBus', 'ListNames'), reply_handler) signal.signal(signal.SIGINT, signal.SIG_DFL) main.run() python-dbus-next-0.2.3/examples/mpris.py000077500000000000000000000025111407735772700203130ustar00rootroot00000000000000#!/usr/bin/env python3 import sys import os sys.path.append(os.path.abspath(os.path.dirname(__file__) + '/..')) from dbus_next.aio import MessageBus import asyncio loop = asyncio.get_event_loop() async def main(): bus = await MessageBus().connect() # the introspection xml would normally be included in your project, but # this is convenient for development introspection = await bus.introspect('org.mpris.MediaPlayer2.vlc', '/org/mpris/MediaPlayer2') obj = bus.get_proxy_object('org.mpris.MediaPlayer2.vlc', '/org/mpris/MediaPlayer2', introspection) player = obj.get_interface('org.mpris.MediaPlayer2.Player') properties = obj.get_interface('org.freedesktop.DBus.Properties') # call methods on the interface (this causes the media player to play) await player.call_play() volume = await player.get_volume() print(f'current volume: {volume}, setting to 0.5') await player.set_volume(0.5) # listen to signals def on_properties_changed(interface_name, changed_properties, invalidated_properties): for changed, variant in changed_properties.items(): print(f'property changed: {changed} - {variant.value}') properties.on_properties_changed(on_properties_changed) await bus.wait_for_disconnect() loop.run_until_complete(main()) python-dbus-next-0.2.3/pytest.ini000066400000000000000000000000251407735772700170150ustar00rootroot00000000000000[pytest] timeout = 5 python-dbus-next-0.2.3/requirements-dev.txt000066400000000000000000000002001407735772700210170ustar00rootroot00000000000000pytest pytest-asyncio pytest-timeout pytest-cov yapf flake8 sphinx sphinx-autobuild sphinxcontrib-asyncio sphinxcontrib-fulltoc python-dbus-next-0.2.3/requirements.txt000066400000000000000000000020031407735772700202460ustar00rootroot00000000000000# This library has no required dependencies. These dependencies are only for # running the test suite and documentation generator. pytest==6.2.2 pytest-asyncio==0.14.0 pytest-timeout==1.4.2 pytest-cov==2.11.1 yapf==0.31.0 flake8==3.9.0 Sphinx==3.5.3 sphinx-autobuild==2021.3.14 sphinxcontrib-asyncio==0.3.0 sphinxcontrib-fulltoc==1.2.0 ## The following requirements were added by pip freeze: alabaster==0.7.12 attrs==20.3.0 Babel==2.9.0 certifi==2020.12.5 chardet==4.0.0 colorama==0.4.4 coverage==5.5 docutils==0.16 idna==2.10 imagesize==1.2.0 iniconfig==1.1.1 Jinja2==2.11.3 livereload==2.6.3 MarkupSafe==1.1.1 mccabe==0.6.1 packaging==20.9 pluggy==0.13.1 py==1.10.0 pycodestyle==2.7.0 pyflakes==2.3.0 Pygments==2.8.1 pyparsing==2.4.7 pytz==2021.1 requests==2.25.1 six==1.15.0 snowballstemmer==2.1.0 sphinxcontrib-applehelp==1.0.2 sphinxcontrib-devhelp==1.0.2 sphinxcontrib-htmlhelp==1.0.3 sphinxcontrib-jsmath==1.0.1 sphinxcontrib-qthelp==1.0.3 sphinxcontrib-serializinghtml==1.1.4 toml==0.10.2 tornado==6.1 urllib3==1.26.5 python-dbus-next-0.2.3/setup.py000066400000000000000000000041641407735772700165060ustar00rootroot00000000000000#!/usr/bin/env python3 # -*- coding: utf-8 -*- import io import os from setuptools import setup, find_packages # Package meta-data. DESCRIPTION = 'A zero-dependency DBus library for Python with asyncio support' REQUIRES_PYTHON = '>=3.6.0' # What packages are required for this module to be executed? REQUIRED = [] # What packages are optional? EXTRAS = {} # The rest you shouldn't have to touch too much :) # ------------------------------------------------ # Except, perhaps the License and Trove Classifiers! # If you do change the License, remember to change the Trove Classifier for that! here = os.path.abspath(os.path.dirname(__file__)) about = {} with open(os.path.join(here, 'dbus_next', '__version__.py')) as f: exec(f.read(), about) with io.open(os.path.join(here, 'README.md'), encoding='utf-8') as f: long_description = '\n' + f.read() setup( name=about['__title__'], version=about['__version__'], description=DESCRIPTION, long_description=long_description, long_description_content_type='text/markdown', author=about['__author__'], author_email=about['__author_email__'], python_requires=REQUIRES_PYTHON, url=about['__url__'], packages=find_packages(exclude=['test', '*.test', '*.test.*', 'test.*']), install_requires=REQUIRED, extras_require=EXTRAS, include_package_data=True, license='MIT', classifiers=[ # Full list: https://pypi.python.org/pypi?%3Aaction=list_classifiers 'Development Status :: 3 - Alpha', 'Environment :: X11 Applications', 'Environment :: X11 Applications :: Gnome', 'Topic :: Desktop Environment :: Gnome', 'Topic :: Software Development :: Embedded Systems', 'Framework :: AsyncIO', 'License :: OSI Approved :: MIT License', 'Programming Language :: Python', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: Implementation :: CPython', 'Programming Language :: Python :: Implementation :: PyPy', 'Typing :: Typed' ]) python-dbus-next-0.2.3/test/000077500000000000000000000000001407735772700157465ustar00rootroot00000000000000python-dbus-next-0.2.3/test/__init__.py000066400000000000000000000000001407735772700200450ustar00rootroot00000000000000python-dbus-next-0.2.3/test/client/000077500000000000000000000000001407735772700172245ustar00rootroot00000000000000python-dbus-next-0.2.3/test/client/test_methods.py000066400000000000000000000076571407735772700223170ustar00rootroot00000000000000from dbus_next.message import MessageFlag from dbus_next.service import ServiceInterface, method import dbus_next.introspection as intr from dbus_next import aio, glib, DBusError from test.util import check_gi_repository, skip_reason_no_gi import pytest has_gi = check_gi_repository() class ExampleInterface(ServiceInterface): def __init__(self): super().__init__('test.interface') @method() def Ping(self): pass @method() def EchoInt64(self, what: 'x') -> 'x': return what @method() def EchoString(self, what: 's') -> 's': return what @method() def ConcatStrings(self, what1: 's', what2: 's') -> 's': return what1 + what2 @method() def EchoThree(self, what1: 's', what2: 's', what3: 's') -> 'sss': return [what1, what2, what3] @method() def ThrowsError(self): raise DBusError('test.error', 'something went wrong') @pytest.mark.asyncio async def test_aio_proxy_object(): bus_name = 'aio.client.test.methods' bus = await aio.MessageBus().connect() bus2 = await aio.MessageBus().connect() await bus.request_name(bus_name) service_interface = ExampleInterface() bus.export('/test/path', service_interface) # add some more to test nodes bus.export('/test/path/child1', ExampleInterface()) bus.export('/test/path/child2', ExampleInterface()) introspection = await bus2.introspect(bus_name, '/test/path') assert type(introspection) is intr.Node obj = bus2.get_proxy_object(bus_name, '/test/path', introspection) interface = obj.get_interface(service_interface.name) children = obj.get_children() assert len(children) == 2 for child in obj.get_children(): assert type(child) is aio.ProxyObject result = await interface.call_ping() assert result is None result = await interface.call_echo_string('hello') assert result == 'hello' result = await interface.call_concat_strings('hello ', 'world') assert result == 'hello world' result = await interface.call_echo_three('hello', 'there', 'world') assert result == ['hello', 'there', 'world'] result = await interface.call_echo_int64(-10000) assert result == -10000 result = await interface.call_echo_string('no reply', flags=MessageFlag.NO_REPLY_EXPECTED) assert result is None with pytest.raises(DBusError): try: await interface.call_throws_error() except DBusError as e: assert e.reply is not None assert e.type == 'test.error' assert e.text == 'something went wrong' raise e bus.disconnect() bus2.disconnect() @pytest.mark.skipif(not has_gi, reason=skip_reason_no_gi) def test_glib_proxy_object(): bus_name = 'glib.client.test.methods' bus = glib.MessageBus().connect_sync() bus.request_name_sync(bus_name) service_interface = ExampleInterface() bus.export('/test/path', service_interface) bus2 = glib.MessageBus().connect_sync() introspection = bus2.introspect_sync(bus_name, '/test/path') assert type(introspection) is intr.Node obj = bus.get_proxy_object(bus_name, '/test/path', introspection) interface = obj.get_interface(service_interface.name) result = interface.call_ping_sync() assert result is None result = interface.call_echo_string_sync('hello') assert result == 'hello' result = interface.call_concat_strings_sync('hello ', 'world') assert result == 'hello world' result = interface.call_echo_three_sync('hello', 'there', 'world') assert result == ['hello', 'there', 'world'] with pytest.raises(DBusError): try: result = interface.call_throws_error_sync() assert False, result except DBusError as e: assert e.reply is not None assert e.type == 'test.error' assert e.text == 'something went wrong' raise e bus.disconnect() bus2.disconnect() python-dbus-next-0.2.3/test/client/test_properties.py000066400000000000000000000076351407735772700230440ustar00rootroot00000000000000from dbus_next import aio, glib, Message, DBusError from dbus_next.service import ServiceInterface, dbus_property, PropertyAccess from test.util import check_gi_repository, skip_reason_no_gi import pytest has_gi = check_gi_repository() class ExampleInterface(ServiceInterface): def __init__(self): super().__init__('test.interface') self._some_property = 'foo' self.error_name = 'test.error' self.error_text = 'i am bad' self._int64_property = -10000 @dbus_property() def SomeProperty(self) -> 's': return self._some_property @SomeProperty.setter def SomeProperty(self, val: 's'): self._some_property = val @dbus_property(access=PropertyAccess.READ) def Int64Property(self) -> 'x': return self._int64_property @dbus_property() def ErrorThrowingProperty(self) -> 's': raise DBusError(self.error_name, self.error_text) @ErrorThrowingProperty.setter def ErrorThrowingProperty(self, val: 's'): raise DBusError(self.error_name, self.error_text) @pytest.mark.asyncio async def test_aio_properties(): service_bus = await aio.MessageBus().connect() service_interface = ExampleInterface() service_bus.export('/test/path', service_interface) bus = await aio.MessageBus().connect() obj = bus.get_proxy_object(service_bus.unique_name, '/test/path', service_bus._introspect_export_path('/test/path')) interface = obj.get_interface(service_interface.name) prop = await interface.get_some_property() assert prop == service_interface._some_property prop = await interface.get_int64_property() assert prop == service_interface._int64_property await interface.set_some_property('different') assert service_interface._some_property == 'different' with pytest.raises(DBusError): try: prop = await interface.get_error_throwing_property() assert False, prop except DBusError as e: assert e.type == service_interface.error_name assert e.text == service_interface.error_text assert type(e.reply) is Message raise e with pytest.raises(DBusError): try: await interface.set_error_throwing_property('different') except DBusError as e: assert e.type == service_interface.error_name assert e.text == service_interface.error_text assert type(e.reply) is Message raise e service_bus.disconnect() bus.disconnect() @pytest.mark.skipif(not has_gi, reason=skip_reason_no_gi) def test_glib_properties(): service_bus = glib.MessageBus().connect_sync() service_interface = ExampleInterface() service_bus.export('/test/path', service_interface) bus = glib.MessageBus().connect_sync() obj = bus.get_proxy_object(service_bus.unique_name, '/test/path', service_bus._introspect_export_path('/test/path')) interface = obj.get_interface(service_interface.name) prop = interface.get_some_property_sync() assert prop == service_interface._some_property interface.set_some_property_sync('different') assert service_interface._some_property == 'different' with pytest.raises(DBusError): try: prop = interface.get_error_throwing_property_sync() assert False, prop except DBusError as e: assert e.type == service_interface.error_name assert e.text == service_interface.error_text assert type(e.reply) is Message raise e with pytest.raises(DBusError): try: interface.set_error_throwing_property_sync('different2') except DBusError as e: assert e.type == service_interface.error_name assert e.text == service_interface.error_text assert type(e.reply) is Message raise e service_bus.disconnect() python-dbus-next-0.2.3/test/client/test_signals.py000066400000000000000000000152301407735772700222760ustar00rootroot00000000000000from dbus_next.service import ServiceInterface, signal from dbus_next.aio import MessageBus from dbus_next import Message from dbus_next.introspection import Node from dbus_next.constants import RequestNameReply import pytest class ExampleInterface(ServiceInterface): def __init__(self): super().__init__('test.interface') @signal() def SomeSignal(self) -> 's': return 'hello' @signal() def SignalMultiple(self) -> 'ss': return ['hello', 'world'] @pytest.mark.asyncio async def test_signals(): bus1 = await MessageBus().connect() bus2 = await MessageBus().connect() bus_intr = await bus1.introspect('org.freedesktop.DBus', '/org/freedesktop/DBus') bus_obj = bus1.get_proxy_object('org.freedesktop.DBus', '/org/freedesktop/DBus', bus_intr) stats = bus_obj.get_interface('org.freedesktop.DBus.Debug.Stats') await bus1.request_name('test.signals.name') service_interface = ExampleInterface() bus1.export('/test/path', service_interface) obj = bus2.get_proxy_object('test.signals.name', '/test/path', bus1._introspect_export_path('/test/path')) interface = obj.get_interface(service_interface.name) async def ping(): await bus2.call( Message(destination=bus1.unique_name, interface='org.freedesktop.DBus.Peer', path='/test/path', member='Ping')) err = None single_counter = 0 def single_handler(value): try: nonlocal single_counter nonlocal err assert value == 'hello' single_counter += 1 except Exception as e: err = e multiple_counter = 0 def multiple_handler(value1, value2): nonlocal multiple_counter nonlocal err try: assert value1 == 'hello' assert value2 == 'world' multiple_counter += 1 except Exception as e: err = e await ping() match_rules = await stats.call_get_all_match_rules() assert bus2.unique_name in match_rules bus_match_rules = match_rules[bus2.unique_name] # the bus connection itself takes a rule on NameOwnerChange after the high # level client is initialized assert len(bus_match_rules) == 1 assert len(bus2._user_message_handlers) == 0 interface.on_some_signal(single_handler) interface.on_signal_multiple(multiple_handler) # Interlude: adding a signal handler with `on_[signal]` should add a match rule and # message handler. Removing a signal handler with `off_[signal]` should # remove the match rule and message handler to avoid memory leaks. await ping() match_rules = await stats.call_get_all_match_rules() assert bus2.unique_name in match_rules bus_match_rules = match_rules[bus2.unique_name] # test the match rule and user handler has been added assert len(bus_match_rules) == 2 assert "type='signal',interface='test.interface',path='/test/path',sender='test.signals.name'" in bus_match_rules assert len(bus2._user_message_handlers) == 1 service_interface.SomeSignal() await ping() assert err is None assert single_counter == 1 service_interface.SignalMultiple() await ping() assert err is None assert multiple_counter == 1 # special case: another bus with the same path and interface but on a # different name and connection will trigger the match rule of the first # (happens with mpris) bus3 = await MessageBus().connect() await bus3.request_name('test.signals.name2') service_interface2 = ExampleInterface() bus3.export('/test/path', service_interface2) obj = bus2.get_proxy_object('test.signals.name2', '/test/path', bus3._introspect_export_path('/test/path')) # we have to add a dummy handler to add the match rule iface2 = obj.get_interface(service_interface2.name) def dummy_signal_handler(what): pass iface2.on_some_signal(dummy_signal_handler) await ping() service_interface2.SomeSignal() await ping() # single_counter is not incremented for signals of the second interface assert single_counter == 1 interface.off_some_signal(single_handler) interface.off_signal_multiple(multiple_handler) iface2.off_some_signal(dummy_signal_handler) # After `off_[signal]`, the match rule and user handler should be removed await ping() match_rules = await stats.call_get_all_match_rules() assert bus2.unique_name in match_rules bus_match_rules = match_rules[bus2.unique_name] assert len(bus_match_rules) == 1 assert "type='signal',interface='test.interface',path='/test/path',sender='test.signals.name'" not in bus_match_rules assert len(bus2._user_message_handlers) == 0 bus1.disconnect() bus2.disconnect() bus3.disconnect() @pytest.mark.asyncio async def test_signals_with_changing_owners(): well_known_name = 'test.signals.changing.name' bus1 = await MessageBus().connect() bus2 = await MessageBus().connect() bus3 = await MessageBus().connect() async def ping(): await bus1.call( Message(destination=bus1.unique_name, interface='org.freedesktop.DBus.Peer', path='/test/path', member='Ping')) service_interface = ExampleInterface() introspection = Node.default() introspection.interfaces.append(service_interface.introspect()) # get the interface before export obj = bus1.get_proxy_object(well_known_name, '/test/path', introspection) iface = obj.get_interface('test.interface') counter = 0 def handler(what): nonlocal counter counter += 1 iface.on_some_signal(handler) await ping() # now export and get the name bus2.export('/test/path', service_interface) result = await bus2.request_name(well_known_name) assert result is RequestNameReply.PRIMARY_OWNER # the signal should work service_interface.SomeSignal() await ping() assert counter == 1 counter = 0 # now queue up a transfer of the name service_interface2 = ExampleInterface() bus3.export('/test/path', service_interface2) result = await bus3.request_name(well_known_name) assert result is RequestNameReply.IN_QUEUE # if it doesn't own the name, the signal shouldn't work here service_interface2.SomeSignal() await ping() assert counter == 0 # now transfer over the name and it should work bus2.disconnect() await ping() service_interface2.SomeSignal() await ping() assert counter == 1 counter = 0 bus1.disconnect() bus2.disconnect() bus3.disconnect() python-dbus-next-0.2.3/test/data/000077500000000000000000000000001407735772700166575ustar00rootroot00000000000000python-dbus-next-0.2.3/test/data/introspection.xml000066400000000000000000000023061407735772700223020ustar00rootroot00000000000000 python-dbus-next-0.2.3/test/data/messages.json000066400000000000000000000233161407735772700213660ustar00rootroot00000000000000[ { "message": { "destination": "org.freedesktop.DBus", "path": "/org/freedesktop/DBus", "interface": "org.freedesktop.DBus", "member": "Hello", "serial": 1, "signature": "" }, "data": "6c01000100000000010000006d00000001016f00150000002f6f72672f667265656465736b746f702f4442757300000002017300140000006f72672e667265656465736b746f702e4442757300000000030173000500000048656c6c6f00000006017300140000006f72672e667265656465736b746f702e4442757300000000" }, { "message": { "destination": "org.freedesktop.DBus", "path": "/org/freedesktop/DBus", "interface": "org.freedesktop.DBus", "member": "Hello", "serial": 1, "signature": "as", "body": [ [ "hello", "world" ] ] }, "data": "6c0100011a000000010000007800000001016f00150000002f6f72672f667265656465736b746f702f4442757300000002017300140000006f72672e667265656465736b746f702e4442757300000000030173000500000048656c6c6f00000006017300140000006f72672e667265656465736b746f702e44427573000000000801670002617300160000000500000068656c6c6f00000005000000776f726c6400" }, { "message": { "destination": "org.freedesktop.DBus", "path": "/org/freedesktop/DBus", "interface": "org.freedesktop.DBus", "member": "Hello", "serial": 1, "signature": "a(uu)", "body": [ [ [ 1, 1 ], [ 2, 2 ] ] ] }, "data": "6c01000118000000010000007b00000001016f00150000002f6f72672f667265656465736b746f702f4442757300000002017300140000006f72672e667265656465736b746f702e4442757300000000030173000500000048656c6c6f00000006017300140000006f72672e667265656465736b746f702e444275730000000008016700056128757529000000000000100000000000000001000000010000000200000002000000" }, { "message": { "destination": "org.freedesktop.DBus", "path": "/org/freedesktop/DBus", "interface": "org.freedesktop.DBus", "member": "Hello", "serial": 1, "signature": "a{ss}", "body": [ { "foo": "bar", "bat": "baz" } ] }, "data": "6c01000128000000010000007b00000001016f00150000002f6f72672f667265656465736b746f702f4442757300000002017300140000006f72672e667265656465736b746f702e4442757300000000030173000500000048656c6c6f00000006017300140000006f72672e667265656465736b746f702e44427573000000000801670005617b73737d000000000000200000000000000003000000666f6f00030000006261720003000000626174000300000062617a00" }, { "message": { "destination": "org.freedesktop.DBus", "path": "/org/freedesktop/DBus", "interface": "org.freedesktop.DBus", "member": "Hello", "serial": 1, "signature": "a(as(uu(a{ss})))", "body": [ [ [ [ "hello", "there" ], [ 5, 6, [ { "five": "six", "seven": "eight" } ] ] ], [ [ "to", "the", "world" ], [ 7, 8, [ { "seven": "eight", "nine": "ten" } ] ] ] ] ] }, "data": "6c010001c4000000010000008600000001016f00150000002f6f72672f667265656465736b746f702f4442757300000002017300140000006f72672e667265656465736b746f702e4442757300000000030173000500000048656c6c6f00000006017300140000006f72672e667265656465736b746f702e444275730000000008016700106128617328757528617b73737d292929000000bc00000000000000160000000500000068656c6c6f0000000500000074686572650000000000000005000000060000002e0000000000000004000000666976650000000003000000736978000000000005000000736576656e0000000500000065696768740000001a00000002000000746f0000030000007468650005000000776f726c6400000007000000080000002c0000000000000005000000736576656e000000050000006569676874000000040000006e696e65000000000300000074656e00" }, { "message": { "destination": "org.freedesktop.DBus", "path": "/org/freedesktop/DBus", "interface": "org.freedesktop.DBus", "member": "Hello", "serial": 1, "signature": "t", "body": [ 9007199254740988 ] }, "data": "6c01000108000000010000007700000001016f00150000002f6f72672f667265656465736b746f702f4442757300000002017300140000006f72672e667265656465736b746f702e4442757300000000030173000500000048656c6c6f00000006017300140000006f72672e667265656465736b746f702e44427573000000000801670001740000fcffffffffff1f00" }, { "message": { "destination": "org.freedesktop.DBus", "path": "/org/freedesktop/DBus", "interface": "org.freedesktop.DBus", "member": "Hello", "serial": 1, "signature": "x", "body": [ -9007199254740988 ] }, "data": "6c01000108000000010000007700000001016f00150000002f6f72672f667265656465736b746f702f4442757300000002017300140000006f72672e667265656465736b746f702e4442757300000000030173000500000048656c6c6f00000006017300140000006f72672e667265656465736b746f702e44427573000000000801670001780000040000000000e0ff" }, { "message": { "destination": "org.freedesktop.DBus", "path": "/org/freedesktop/DBus", "interface": "org.freedesktop.DBus", "member": "Hello", "serial": 1, "signature": "bnqiud", "body": [ true, -200, 150, -20000, 20000, 9083492084.4444 ] }, "data": "6c01000118000000010000007c00000001016f00150000002f6f72672f667265656465736b746f702f4442757300000002017300140000006f72672e667265656465736b746f702e4442757300000000030173000500000048656c6c6f00000006017300140000006f72672e667265656465736b746f702e44427573000000000801670006626e7169756400000000000100000038ff9600e0b1ffff204e0000228ea3b758eb0042" }, { "message": { "destination": "org.freedesktop.DBus", "path": "/org/freedesktop/DBus", "interface": "org.freedesktop.DBus", "member": "Hello", "serial": 1, "signature": "v", "body": [ { "signature": "s", "value": "hello world" } ] }, "data": "6c01000114000000010000007700000001016f00150000002f6f72672f667265656465736b746f702f4442757300000002017300140000006f72672e667265656465736b746f702e4442757300000000030173000500000048656c6c6f00000006017300140000006f72672e667265656465736b746f702e44427573000000000801670001760000017300000b00000068656c6c6f20776f726c6400" }, { "message": { "destination": "org.freedesktop.DBus", "path": "/org/freedesktop/DBus", "interface": "org.freedesktop.DBus", "member": "Hello", "serial": 1, "signature": "v", "body": [ { "signature": "v", "value": { "signature": "s", "value": "hello" } } ] }, "data": "6c01000112000000010000007700000001016f00150000002f6f72672f667265656465736b746f702f4442757300000002017300140000006f72672e667265656465736b746f702e4442757300000000030173000500000048656c6c6f00000006017300140000006f72672e667265656465736b746f702e4442757300000000080167000176000001760001730000000500000068656c6c6f00" }, { "message": { "destination": "org.freedesktop.DBus", "path": "/org/freedesktop/DBus", "interface": "org.freedesktop.DBus", "member": "Hello", "serial": 1, "signature": "a{sv}", "body": [ { "variant_key_1": { "signature": "s", "value": "variant_val_1" }, "variant_key_2": { "signature": "s", "value": "variant_val_2" } } ] }, "data": "6c01000162000000010000007b00000001016f00150000002f6f72672f667265656465736b746f702f4442757300000002017300140000006f72672e667265656465736b746f702e4442757300000000030173000500000048656c6c6f00000006017300140000006f72672e667265656465736b746f702e44427573000000000801670005617b73767d0000000000005a000000000000000d00000076617269616e745f6b65795f31000173000000000d00000076617269616e745f76616c5f31000000000000000d00000076617269616e745f6b65795f32000173000000000d00000076617269616e745f76616c5f3200" }, { "message": { "destination": "org.freedesktop.DBus", "path": "/org/freedesktop/DBus", "interface": "org.freedesktop.DBus", "member": "Hello", "serial": 1, "signature": "v", "body": [ { "signature": "as", "value": [ "foo", "bar" ] } ] }, "data": "6c01000118000000010000007700000001016f00150000002f6f72672f667265656465736b746f702f4442757300000002017300140000006f72672e667265656465736b746f702e4442757300000000030173000500000048656c6c6f00000006017300140000006f72672e667265656465736b746f702e44427573000000000801670001760000026173001000000003000000666f6f000300000062617200" }, { "message": { "destination": "org.freedesktop.DBus", "path": "/org/freedesktop/DBus", "interface": "org.freedesktop.DBus", "member": "Hello", "serial": 1, "signature": "vas", "body": [ { "signature": "v", "value": { "signature": "s", "value": "world" } }, [ "bar" ] ] }, "data": "6c01000120000000010000007900000001016f00150000002f6f72672f667265656465736b746f702f4442757300000002017300140000006f72672e667265656465736b746f702e4442757300000000030173000500000048656c6c6f00000006017300140000006f72672e667265656465736b746f702e444275730000000008016700037661730000000000000000017600017300000005000000776f726c64000000080000000300000062617200" } ] python-dbus-next-0.2.3/test/service/000077500000000000000000000000001407735772700174065ustar00rootroot00000000000000python-dbus-next-0.2.3/test/service/__init__.py000066400000000000000000000000001407735772700215050ustar00rootroot00000000000000python-dbus-next-0.2.3/test/service/test_decorators.py000066400000000000000000000110671407735772700231710ustar00rootroot00000000000000from dbus_next import PropertyAccess, introspection as intr from dbus_next.service import method, signal, dbus_property, ServiceInterface class ExampleInterface(ServiceInterface): def __init__(self): super().__init__('test.interface') self._some_prop = 55 self._another_prop = 101 self._weird_prop = 500 @method() def some_method(self, one: 's', two: 's') -> 's': return 'hello' @method(name='renamed_method', disabled=True) def another_method(self, eight: 'o', six: 't'): pass @signal() def some_signal(self) -> 'as': return ['result'] @signal(name='renamed_signal', disabled=True) def another_signal(self) -> '(dodo)': return [1, '/', 1, '/'] @dbus_property(name='renamed_readonly_property', access=PropertyAccess.READ, disabled=True) def another_prop(self) -> 't': return self._another_prop @dbus_property() def some_prop(self) -> 'u': return self._some_prop @some_prop.setter def some_prop(self, val: 'u'): self._some_prop = val + 1 # for this one, the setter has a different name than the getter which is a # special case in the code @dbus_property() def weird_prop(self) -> 't': return self._weird_prop @weird_prop.setter def setter_for_weird_prop(self, val: 't'): self._weird_prop = val def test_method_decorator(): interface = ExampleInterface() assert interface.name == 'test.interface' properties = ServiceInterface._get_properties(interface) methods = ServiceInterface._get_methods(interface) signals = ServiceInterface._get_signals(interface) assert len(methods) == 2 method = methods[0] assert method.name == 'renamed_method' assert method.in_signature == 'ot' assert method.out_signature == '' assert method.disabled assert type(method.introspection) is intr.Method method = methods[1] assert method.name == 'some_method' assert method.in_signature == 'ss' assert method.out_signature == 's' assert not method.disabled assert type(method.introspection) is intr.Method assert len(signals) == 2 signal = signals[0] assert signal.name == 'renamed_signal' assert signal.signature == '(dodo)' assert signal.disabled assert type(signal.introspection) is intr.Signal signal = signals[1] assert signal.name == 'some_signal' assert signal.signature == 'as' assert not signal.disabled assert type(signal.introspection) is intr.Signal assert len(properties) == 3 renamed_readonly_prop = properties[0] assert renamed_readonly_prop.name == 'renamed_readonly_property' assert renamed_readonly_prop.signature == 't' assert renamed_readonly_prop.access == PropertyAccess.READ assert renamed_readonly_prop.disabled assert type(renamed_readonly_prop.introspection) is intr.Property weird_prop = properties[1] assert weird_prop.name == 'weird_prop' assert weird_prop.access == PropertyAccess.READWRITE assert weird_prop.signature == 't' assert not weird_prop.disabled assert weird_prop.prop_getter is not None assert weird_prop.prop_getter.__name__ == 'weird_prop' assert weird_prop.prop_setter is not None assert weird_prop.prop_setter.__name__ == 'setter_for_weird_prop' assert type(weird_prop.introspection) is intr.Property prop = properties[2] assert prop.name == 'some_prop' assert prop.access == PropertyAccess.READWRITE assert prop.signature == 'u' assert not prop.disabled assert prop.prop_getter is not None assert prop.prop_setter is not None assert type(prop.introspection) is intr.Property # make sure the getter and setter actually work assert interface._some_prop == 55 interface._some_prop = 555 assert interface.some_prop == 555 assert interface._weird_prop == 500 assert weird_prop.prop_getter(interface) == 500 interface._weird_prop = 1001 assert interface._weird_prop == 1001 weird_prop.prop_setter(interface, 600) assert interface._weird_prop == 600 def test_interface_introspection(): interface = ExampleInterface() intr_interface = interface.introspect() assert type(intr_interface) is intr.Interface xml = intr_interface.to_xml() assert xml.tag == 'interface' assert xml.attrib.get('name', None) == 'test.interface' methods = xml.findall('method') signals = xml.findall('signal') properties = xml.findall('property') assert len(xml) == 4 assert len(methods) == 1 assert len(signals) == 1 assert len(properties) == 2 python-dbus-next-0.2.3/test/service/test_export.py000066400000000000000000000065361407735772700223520ustar00rootroot00000000000000from dbus_next.service import ServiceInterface, method from dbus_next.aio import MessageBus from dbus_next import Message, MessageType, introspection as intr import pytest standard_interfaces_count = len(intr.Node.default().interfaces) class ExampleInterface(ServiceInterface): def __init__(self, name): self._method_called = False super().__init__(name) @method() def some_method(self): self._method_called = True @pytest.mark.asyncio async def test_export_unexport(): interface = ExampleInterface('test.interface') interface2 = ExampleInterface('test.interface2') export_path = '/test/path' export_path2 = '/test/path/child' bus = await MessageBus().connect() bus.export(export_path, interface) assert export_path in bus._path_exports assert len(bus._path_exports[export_path]) == 1 assert bus._path_exports[export_path][0] is interface assert len(ServiceInterface._get_buses(interface)) == 1 bus.export(export_path2, interface2) node = bus._introspect_export_path(export_path) assert len(node.interfaces) == standard_interfaces_count + 1 assert len(node.nodes) == 1 # relative path assert node.nodes[0].name == 'child' bus.unexport(export_path, interface) assert export_path not in bus._path_exports assert len(ServiceInterface._get_buses(interface)) == 0 bus.export(export_path2, interface) assert len(bus._path_exports[export_path2]) == 2 # test unexporting the whole path bus.unexport(export_path2) assert not bus._path_exports assert not ServiceInterface._get_buses(interface) assert not ServiceInterface._get_buses(interface2) # test unexporting by name bus.export(export_path, interface) bus.unexport(export_path, interface.name) assert not bus._path_exports assert not ServiceInterface._get_buses(interface) node = bus._introspect_export_path('/path/doesnt/exist') assert type(node) is intr.Node assert not node.interfaces assert not node.nodes @pytest.mark.asyncio async def test_export_alias(): bus = await MessageBus().connect() interface = ExampleInterface('test.interface') export_path = '/test/path' export_path2 = '/test/path/child' bus.export(export_path, interface) bus.export(export_path2, interface) result = await bus.call( Message(destination=bus.unique_name, path=export_path, interface='test.interface', member='some_method')) assert result.message_type is MessageType.METHOD_RETURN, result.body[0] assert interface._method_called interface._method_called = False result = await bus.call( Message(destination=bus.unique_name, path=export_path2, interface='test.interface', member='some_method')) assert result.message_type is MessageType.METHOD_RETURN, result.body[0] assert interface._method_called @pytest.mark.asyncio async def test_export_introspection(): interface = ExampleInterface('test.interface') interface2 = ExampleInterface('test.interface2') export_path = '/test/path' export_path2 = '/test/path/child' bus = await MessageBus().connect() bus.export(export_path, interface) bus.export(export_path2, interface2) root = bus._introspect_export_path('/') assert len(root.nodes) == 1 python-dbus-next-0.2.3/test/service/test_methods.py000066400000000000000000000122141407735772700224620ustar00rootroot00000000000000from dbus_next.service import ServiceInterface, method from dbus_next.aio import MessageBus from dbus_next import Message, MessageType, ErrorType, Variant, SignatureTree, DBusError, MessageFlag import pytest class ExampleInterface(ServiceInterface): def __init__(self, name): super().__init__(name) @method() def echo(self, what: 's') -> 's': assert type(self) is ExampleInterface return what @method() def echo_multiple(self, what1: 's', what2: 's') -> 'ss': assert type(self) is ExampleInterface return [what1, what2] @method() def echo_containers(self, array: 'as', variant: 'v', dict_entries: 'a{sv}', struct: '(s(s(v)))') -> 'asva{sv}(s(s(v)))': assert type(self) is ExampleInterface return [array, variant, dict_entries, struct] @method() def ping(self): assert type(self) is ExampleInterface pass @method(name='renamed') def original_name(self): assert type(self) is ExampleInterface pass @method(disabled=True) def not_here(self): assert type(self) is ExampleInterface pass @method() def throws_unexpected_error(self): assert type(self) is ExampleInterface raise Exception('oops') @method() def throws_dbus_error(self): assert type(self) is ExampleInterface raise DBusError('test.error', 'an error ocurred') class AsyncInterface(ServiceInterface): def __init__(self, name): super().__init__(name) @method() async def echo(self, what: 's') -> 's': assert type(self) is AsyncInterface return what @method() async def echo_multiple(self, what1: 's', what2: 's') -> 'ss': assert type(self) is AsyncInterface return [what1, what2] @method() async def echo_containers(self, array: 'as', variant: 'v', dict_entries: 'a{sv}', struct: '(s(s(v)))') -> 'asva{sv}(s(s(v)))': assert type(self) is AsyncInterface return [array, variant, dict_entries, struct] @method() async def ping(self): assert type(self) is AsyncInterface pass @method(name='renamed') async def original_name(self): assert type(self) is AsyncInterface pass @method(disabled=True) async def not_here(self): assert type(self) is AsyncInterface pass @method() async def throws_unexpected_error(self): assert type(self) is AsyncInterface raise Exception('oops') @method() def throws_dbus_error(self): assert type(self) is AsyncInterface raise DBusError('test.error', 'an error ocurred') @pytest.mark.parametrize('interface_class', [ExampleInterface, AsyncInterface]) @pytest.mark.asyncio async def test_methods(interface_class): bus1 = await MessageBus().connect() bus2 = await MessageBus().connect() interface = interface_class('test.interface') export_path = '/test/path' async def call(member, signature='', body=[], flags=MessageFlag.NONE): return await bus2.call( Message(destination=bus1.unique_name, path=export_path, interface=interface.name, member=member, signature=signature, body=body, flags=flags)) bus1.export(export_path, interface) body = ['hello world'] reply = await call('echo', 's', body) assert reply.message_type == MessageType.METHOD_RETURN, reply.body[0] assert reply.signature == 's' assert reply.body == body body = ['hello', 'world'] reply = await call('echo_multiple', 'ss', body) assert reply.message_type == MessageType.METHOD_RETURN, reply.body[0] assert reply.signature == 'ss' assert reply.body == body body = [['hello', 'world'], Variant('v', Variant('(ss)', ['hello', 'world'])), { 'foo': Variant('t', 100) }, ['one', ['two', [Variant('s', 'three')]]]] signature = 'asva{sv}(s(s(v)))' SignatureTree(signature).verify(body) reply = await call('echo_containers', signature, body) assert reply.message_type == MessageType.METHOD_RETURN, reply.body[0] assert reply.signature == signature assert reply.body == body reply = await call('ping') assert reply.message_type == MessageType.METHOD_RETURN, reply.body[0] assert reply.signature == '' assert reply.body == [] reply = await call('throws_unexpected_error') assert reply.message_type == MessageType.ERROR, reply.body[0] assert reply.error_name == ErrorType.SERVICE_ERROR.value, reply.body[0] reply = await call('throws_dbus_error') assert reply.message_type == MessageType.ERROR, reply.body[0] assert reply.error_name == 'test.error', reply.body[0] assert reply.body == ['an error ocurred'] reply = await call('ping', flags=MessageFlag.NO_REPLY_EXPECTED) assert reply is None reply = await call('throws_unexpected_error', flags=MessageFlag.NO_REPLY_EXPECTED) assert reply is None reply = await call('throws_dbus_error', flags=MessageFlag.NO_REPLY_EXPECTED) assert reply is None python-dbus-next-0.2.3/test/service/test_properties.py000066400000000000000000000223401407735772700232140ustar00rootroot00000000000000from dbus_next.service import ServiceInterface, dbus_property, method from dbus_next.aio import MessageBus from dbus_next import Message, MessageType, PropertyAccess, ErrorType, Variant, DBusError import pytest import asyncio class ExampleInterface(ServiceInterface): def __init__(self, name): super().__init__(name) self._string_prop = 'hi' self._readonly_prop = 100 self._disabled_prop = '1234' self._container_prop = [['hello', 'world']] self._renamed_prop = '65' @dbus_property() def string_prop(self) -> 's': return self._string_prop @string_prop.setter def string_prop_setter(self, val: 's'): self._string_prop = val @dbus_property(PropertyAccess.READ) def readonly_prop(self) -> 't': return self._readonly_prop @dbus_property() def container_prop(self) -> 'a(ss)': return self._container_prop @container_prop.setter def container_prop(self, val: 'a(ss)'): self._container_prop = val @dbus_property(name='renamed_prop') def original_name(self) -> 's': return self._renamed_prop @original_name.setter def original_name_setter(self, val: 's'): self._renamed_prop = val @dbus_property(disabled=True) def disabled_prop(self) -> 's': return self._disabled_prop @disabled_prop.setter def disabled_prop(self, val: 's'): self._disabled_prop = val @dbus_property(disabled=True) def throws_error(self) -> 's': raise DBusError('test.error', 'told you so') @throws_error.setter def throws_error(self, val: 's'): raise DBusError('test.error', 'told you so') @dbus_property(PropertyAccess.READ, disabled=True) def returns_wrong_type(self) -> 's': return 5 @method() def do_emit_properties_changed(self): changed = {'string_prop': 'asdf'} invalidated = ['container_prop'] self.emit_properties_changed(changed, invalidated) class AsyncInterface(ServiceInterface): def __init__(self, name): super().__init__(name) self._string_prop = 'hi' self._readonly_prop = 100 self._disabled_prop = '1234' self._container_prop = [['hello', 'world']] self._renamed_prop = '65' @dbus_property() async def string_prop(self) -> 's': return self._string_prop @string_prop.setter async def string_prop_setter(self, val: 's'): self._string_prop = val @dbus_property(PropertyAccess.READ) async def readonly_prop(self) -> 't': return self._readonly_prop @dbus_property() async def container_prop(self) -> 'a(ss)': return self._container_prop @container_prop.setter async def container_prop(self, val: 'a(ss)'): self._container_prop = val @dbus_property(name='renamed_prop') async def original_name(self) -> 's': return self._renamed_prop @original_name.setter async def original_name_setter(self, val: 's'): self._renamed_prop = val @dbus_property(disabled=True) async def disabled_prop(self) -> 's': return self._disabled_prop @disabled_prop.setter async def disabled_prop(self, val: 's'): self._disabled_prop = val @dbus_property(disabled=True) async def throws_error(self) -> 's': raise DBusError('test.error', 'told you so') @throws_error.setter async def throws_error(self, val: 's'): raise DBusError('test.error', 'told you so') @dbus_property(PropertyAccess.READ, disabled=True) async def returns_wrong_type(self) -> 's': return 5 @method() def do_emit_properties_changed(self): changed = {'string_prop': 'asdf'} invalidated = ['container_prop'] self.emit_properties_changed(changed, invalidated) @pytest.mark.parametrize('interface_class', [ExampleInterface, AsyncInterface]) @pytest.mark.asyncio async def test_property_methods(interface_class): bus1 = await MessageBus().connect() bus2 = await MessageBus().connect() interface = interface_class('test.interface') export_path = '/test/path' bus1.export(export_path, interface) async def call_properties(member, signature, body): return await bus2.call( Message(destination=bus1.unique_name, path=export_path, interface='org.freedesktop.DBus.Properties', member=member, signature=signature, body=body)) result = await call_properties('GetAll', 's', [interface.name]) assert result.message_type == MessageType.METHOD_RETURN, result.body[0] assert result.signature == 'a{sv}' assert result.body == [{ 'string_prop': Variant('s', interface._string_prop), 'readonly_prop': Variant('t', interface._readonly_prop), 'container_prop': Variant('a(ss)', interface._container_prop), 'renamed_prop': Variant('s', interface._renamed_prop) }] result = await call_properties('Get', 'ss', [interface.name, 'string_prop']) assert result.message_type == MessageType.METHOD_RETURN, result.body[0] assert result.signature == 'v' assert result.body == [Variant('s', 'hi')] result = await call_properties( 'Set', 'ssv', [interface.name, 'string_prop', Variant('s', 'ho')]) assert result.message_type == MessageType.METHOD_RETURN, result.body[0] assert interface._string_prop == 'ho' if interface_class is AsyncInterface: assert 'ho', await interface.string_prop() else: assert 'ho', interface.string_prop result = await call_properties( 'Set', 'ssv', [interface.name, 'readonly_prop', Variant('t', 100)]) assert result.message_type == MessageType.ERROR, result.body[0] assert result.error_name == ErrorType.PROPERTY_READ_ONLY.value, result.body[0] result = await call_properties( 'Set', 'ssv', [interface.name, 'disabled_prop', Variant('s', 'asdf')]) assert result.message_type == MessageType.ERROR, result.body[0] assert result.error_name == ErrorType.UNKNOWN_PROPERTY.value result = await call_properties( 'Set', 'ssv', [interface.name, 'not_a_prop', Variant('s', 'asdf')]) assert result.message_type == MessageType.ERROR, result.body[0] assert result.error_name == ErrorType.UNKNOWN_PROPERTY.value # wrong type result = await call_properties('Set', 'ssv', [interface.name, 'string_prop', Variant('t', 100)]) assert result.message_type == MessageType.ERROR assert result.error_name == ErrorType.INVALID_SIGNATURE.value # enable the erroring properties so we can test them for prop in ServiceInterface._get_properties(interface): if prop.name in ['throws_error', 'returns_wrong_type']: prop.disabled = False result = await call_properties('Get', 'ss', [interface.name, 'returns_wrong_type']) assert result.message_type == MessageType.ERROR, result.body[0] assert result.error_name == ErrorType.SERVICE_ERROR.value result = await call_properties( 'Set', 'ssv', [interface.name, 'throws_error', Variant('s', 'ho')]) assert result.message_type == MessageType.ERROR, result.body[0] assert result.error_name == 'test.error' assert result.body == ['told you so'] result = await call_properties('Get', 'ss', [interface.name, 'throws_error']) assert result.message_type == MessageType.ERROR, result.body[0] assert result.error_name == 'test.error' assert result.body == ['told you so'] result = await call_properties('GetAll', 's', [interface.name]) assert result.message_type == MessageType.ERROR, result.body[0] assert result.error_name == 'test.error' assert result.body == ['told you so'] @pytest.mark.parametrize('interface_class', [ExampleInterface, AsyncInterface]) @pytest.mark.asyncio async def test_property_changed_signal(interface_class): bus1 = await MessageBus().connect() bus2 = await MessageBus().connect() await bus2.call( Message(destination='org.freedesktop.DBus', path='/org/freedesktop/DBus', interface='org.freedesktop.DBus', member='AddMatch', signature='s', body=[f'sender={bus1.unique_name}'])) interface = interface_class('test.interface') export_path = '/test/path' bus1.export(export_path, interface) async def wait_for_message(): # TODO timeout future = asyncio.get_event_loop().create_future() def message_handler(signal): if signal.interface == 'org.freedesktop.DBus.Properties': bus2.remove_message_handler(message_handler) future.set_result(signal) bus2.add_message_handler(message_handler) return await future bus2.send( Message(destination=bus1.unique_name, interface=interface.name, path=export_path, member='do_emit_properties_changed')) signal = await wait_for_message() assert signal.interface == 'org.freedesktop.DBus.Properties' assert signal.member == 'PropertiesChanged' assert signal.signature == 'sa{sv}as' assert signal.body == [ interface.name, { 'string_prop': Variant('s', 'asdf') }, ['container_prop'] ] python-dbus-next-0.2.3/test/service/test_signals.py000066400000000000000000000166461407735772700224740ustar00rootroot00000000000000from dbus_next.service import ServiceInterface, signal, SignalDisabledError, dbus_property from dbus_next.aio import MessageBus from dbus_next import Message, MessageType from dbus_next.constants import PropertyAccess from dbus_next.signature import Variant import pytest import asyncio class ExampleInterface(ServiceInterface): def __init__(self, name): super().__init__(name) @signal() def signal_empty(self): assert type(self) is ExampleInterface @signal() def signal_simple(self) -> 's': assert type(self) is ExampleInterface return 'hello' @signal() def signal_multiple(self) -> 'ss': assert type(self) is ExampleInterface return ['hello', 'world'] @signal(name='renamed') def original_name(self): assert type(self) is ExampleInterface @signal(disabled=True) def signal_disabled(self): assert type(self) is ExampleInterface @dbus_property(access=PropertyAccess.READ) def test_prop(self) -> 'i': return 42 class SecondExampleInterface(ServiceInterface): def __init__(self, name): super().__init__(name) @dbus_property(access=PropertyAccess.READ) def str_prop(self) -> 's': return "abc" @dbus_property(access=PropertyAccess.READ) def list_prop(self) -> 'ai': return [1, 2, 3] class ExpectMessage: def __init__(self, bus1, bus2, interface_name, timeout=1): self.future = asyncio.get_event_loop().create_future() self.bus1 = bus1 self.bus2 = bus2 self.interface_name = interface_name self.timeout = timeout self.timeout_task = None def message_handler(self, msg): if msg.sender == self.bus1.unique_name and msg.interface == self.interface_name: self.timeout_task.cancel() self.future.set_result(msg) return True def timeout_cb(self): self.future.set_exception(TimeoutError) async def __aenter__(self): self.bus2.add_message_handler(self.message_handler) self.timeout_task = asyncio.get_event_loop().call_later(self.timeout, self.timeout_cb) return self.future async def __aexit__(self, exc_type, exc_val, exc_tb): self.bus2.remove_message_handler(self.message_handler) def assert_signal_ok(signal, export_path, member, signature, body): assert signal.message_type == MessageType.SIGNAL assert signal.path == export_path assert signal.member == member assert signal.signature == signature assert signal.body == body @pytest.mark.asyncio async def test_signals(): bus1 = await MessageBus().connect() bus2 = await MessageBus().connect() interface = ExampleInterface('test.interface') export_path = '/test/path' bus1.export(export_path, interface) await bus2.call( Message(destination='org.freedesktop.DBus', path='/org/freedesktop/DBus', interface='org.freedesktop.DBus', member='AddMatch', signature='s', body=[f'sender={bus1.unique_name}'])) async with ExpectMessage(bus1, bus2, interface.name) as expected_signal: interface.signal_empty() assert_signal_ok(signal=await expected_signal, export_path=export_path, member='signal_empty', signature='', body=[]) async with ExpectMessage(bus1, bus2, interface.name) as expected_signal: interface.original_name() assert_signal_ok(signal=await expected_signal, export_path=export_path, member='renamed', signature='', body=[]) async with ExpectMessage(bus1, bus2, interface.name) as expected_signal: interface.signal_simple() assert_signal_ok(signal=await expected_signal, export_path=export_path, member='signal_simple', signature='s', body=['hello']) async with ExpectMessage(bus1, bus2, interface.name) as expected_signal: interface.signal_multiple() assert_signal_ok(signal=await expected_signal, export_path=export_path, member='signal_multiple', signature='ss', body=['hello', 'world']) with pytest.raises(SignalDisabledError): interface.signal_disabled() @pytest.mark.asyncio async def test_interface_add_remove_signal(): bus1 = await MessageBus().connect() bus2 = await MessageBus().connect() await bus2.call( Message(destination='org.freedesktop.DBus', path='/org/freedesktop/DBus', interface='org.freedesktop.DBus', member='AddMatch', signature='s', body=[f'sender={bus1.unique_name}'])) first_interface = ExampleInterface('test.interface.first') second_interface = SecondExampleInterface('test.interface.second') export_path = '/test/path' # add first interface async with ExpectMessage(bus1, bus2, 'org.freedesktop.DBus.ObjectManager') as expected_signal: bus1.export(export_path, first_interface) assert_signal_ok( signal=await expected_signal, export_path=export_path, member='InterfacesAdded', signature='oa{sa{sv}}', body=[export_path, { 'test.interface.first': { 'test_prop': Variant('i', 42) } }]) # add second interface async with ExpectMessage(bus1, bus2, 'org.freedesktop.DBus.ObjectManager') as expected_signal: bus1.export(export_path, second_interface) assert_signal_ok(signal=await expected_signal, export_path=export_path, member='InterfacesAdded', signature='oa{sa{sv}}', body=[ export_path, { 'test.interface.second': { 'str_prop': Variant('s', "abc"), 'list_prop': Variant('ai', [1, 2, 3]) } } ]) # remove single interface async with ExpectMessage(bus1, bus2, 'org.freedesktop.DBus.ObjectManager') as expected_signal: bus1.unexport(export_path, second_interface) assert_signal_ok(signal=await expected_signal, export_path=export_path, member='InterfacesRemoved', signature='oas', body=[export_path, ['test.interface.second']]) # add second interface again async with ExpectMessage(bus1, bus2, 'org.freedesktop.DBus.ObjectManager') as expected_signal: bus1.export(export_path, second_interface) await expected_signal # remove multiple interfaces async with ExpectMessage(bus1, bus2, 'org.freedesktop.DBus.ObjectManager') as expected_signal: bus1.unexport(export_path) assert_signal_ok(signal=await expected_signal, export_path=export_path, member='InterfacesRemoved', signature='oas', body=[export_path, ['test.interface.first', 'test.interface.second']]) python-dbus-next-0.2.3/test/service/test_standard_interfaces.py000066400000000000000000000155241407735772700250310ustar00rootroot00000000000000from dbus_next.service import ServiceInterface, dbus_property, PropertyAccess from dbus_next.signature import Variant from dbus_next.aio import MessageBus from dbus_next import Message, MessageType, introspection as intr from dbus_next.constants import ErrorType import pytest standard_interfaces_count = len(intr.Node.default().interfaces) class ExampleInterface(ServiceInterface): def __init__(self, name): super().__init__(name) class ExampleComplexInterface(ServiceInterface): def __init__(self, name): self._foo = 42 self._bar = 'str' self._async_prop = 'async' super().__init__(name) @dbus_property(access=PropertyAccess.READ) def Foo(self) -> 'y': return self._foo @dbus_property(access=PropertyAccess.READ) def Bar(self) -> 's': return self._bar @dbus_property(access=PropertyAccess.READ) async def AsyncProp(self) -> 's': return self._async_prop @pytest.mark.asyncio async def test_introspectable_interface(): bus1 = await MessageBus().connect() bus2 = await MessageBus().connect() interface = ExampleInterface('test.interface') interface2 = ExampleInterface('test.interface2') export_path = '/test/path' bus1.export(export_path, interface) bus1.export(export_path, interface2) reply = await bus2.call( Message(destination=bus1.unique_name, path=export_path, interface='org.freedesktop.DBus.Introspectable', member='Introspect')) assert reply.message_type == MessageType.METHOD_RETURN, reply.body[0] assert reply.signature == 's' node = intr.Node.parse(reply.body[0]) assert len(node.interfaces) == standard_interfaces_count + 2 assert node.interfaces[-1].name == 'test.interface2' assert node.interfaces[-2].name == 'test.interface' assert not node.nodes # introspect works on every path reply = await bus2.call( Message(destination=bus1.unique_name, path='/path/doesnt/exist', interface='org.freedesktop.DBus.Introspectable', member='Introspect')) assert reply.message_type == MessageType.METHOD_RETURN, reply.body[0] assert reply.signature == 's' node = intr.Node.parse(reply.body[0]) assert not node.interfaces assert not node.nodes @pytest.mark.asyncio async def test_peer_interface(): bus1 = await MessageBus().connect() bus2 = await MessageBus().connect() reply = await bus2.call( Message(destination=bus1.unique_name, path='/path/doesnt/exist', interface='org.freedesktop.DBus.Peer', member='Ping')) assert reply.message_type == MessageType.METHOD_RETURN, reply.body[0] assert reply.signature == '' reply = await bus2.call( Message(destination=bus1.unique_name, path='/path/doesnt/exist', interface='org.freedesktop.DBus.Peer', member='GetMachineId', signature='')) assert reply.message_type == MessageType.METHOD_RETURN, reply.body[0] assert reply.signature == 's' @pytest.mark.asyncio async def test_object_manager(): expected_reply = { '/test/path/deeper': { 'test.interface2': { 'Bar': Variant('s', 'str'), 'Foo': Variant('y', 42), 'AsyncProp': Variant('s', 'async'), } } } reply_ext = { '/test/path': { 'test.interface1': {}, 'test.interface2': { 'Bar': Variant('s', 'str'), 'Foo': Variant('y', 42), 'AsyncProp': Variant('s', 'async'), } } } bus1 = await MessageBus().connect() bus2 = await MessageBus().connect() interface = ExampleInterface('test.interface1') interface2 = ExampleComplexInterface('test.interface2') export_path = '/test/path' bus1.export(export_path, interface) bus1.export(export_path, interface2) bus1.export(export_path + '/deeper', interface2) reply_root = await bus2.call( Message(destination=bus1.unique_name, path='/', interface='org.freedesktop.DBus.ObjectManager', member='GetManagedObjects')) reply_level1 = await bus2.call( Message(destination=bus1.unique_name, path=export_path, interface='org.freedesktop.DBus.ObjectManager', member='GetManagedObjects')) reply_level2 = await bus2.call( Message(destination=bus1.unique_name, path=export_path + '/deeper', interface='org.freedesktop.DBus.ObjectManager', member='GetManagedObjects')) assert reply_root.signature == 'a{oa{sa{sv}}}' assert reply_level1.signature == 'a{oa{sa{sv}}}' assert reply_level2.signature == 'a{oa{sa{sv}}}' assert reply_level2.body == [{}] assert reply_level1.body == [expected_reply] expected_reply.update(reply_ext) assert reply_root.body == [expected_reply] @pytest.mark.asyncio async def test_standard_interface_properties(): # standard interfaces have no properties, but should still behave correctly # when you try to call the methods anyway (#49) bus1 = await MessageBus().connect() bus2 = await MessageBus().connect() interface = ExampleInterface('test.interface1') export_path = '/test/path' bus1.export(export_path, interface) for iface in [ 'org.freedesktop.DBus.Properties', 'org.freedesktop.DBus.Introspectable', 'org.freedesktop.DBus.Peer', 'org.freedesktop.DBus.ObjectManager' ]: result = await bus2.call( Message(destination=bus1.unique_name, path=export_path, interface='org.freedesktop.DBus.Properties', member='Get', signature='ss', body=[iface, 'anything'])) assert result.message_type is MessageType.ERROR assert result.error_name == ErrorType.UNKNOWN_PROPERTY.value result = await bus2.call( Message(destination=bus1.unique_name, path=export_path, interface='org.freedesktop.DBus.Properties', member='Set', signature='ssv', body=[iface, 'anything', Variant('s', 'new thing')])) assert result.message_type is MessageType.ERROR assert result.error_name == ErrorType.UNKNOWN_PROPERTY.value result = await bus2.call( Message(destination=bus1.unique_name, path=export_path, interface='org.freedesktop.DBus.Properties', member='GetAll', signature='s', body=[iface])) assert result.message_type is MessageType.METHOD_RETURN assert result.body == [{}] python-dbus-next-0.2.3/test/test_address_parser.py000066400000000000000000000016411407735772700223620ustar00rootroot00000000000000from dbus_next._private.address import parse_address def test_valid_addresses(): valid_addresses = { 'unix:path=/run/user/1000/bus': [('unix', { 'path': '/run/user/1000/bus' })], 'unix:abstract=/tmp/dbus-ft9sODWpZk,guid=a7b1d5912379c2d471165e9b5cb74a03': [('unix', { 'abstract': '/tmp/dbus-ft9sODWpZk', 'guid': 'a7b1d5912379c2d471165e9b5cb74a03' })], 'unix1:key1=val1;unix2:key2=val2': [('unix1', { 'key1': 'val1' }), ('unix2', { 'key2': 'val2' })], 'unix:escaped=hello%20world': [('unix', { 'escaped': 'hello world' })], 'tcp:host=127.0.0.1,port=55556': [('tcp', { 'host': '127.0.0.1', 'port': '55556' })] } for address, parsed in valid_addresses.items(): assert parse_address(address) == parsed python-dbus-next-0.2.3/test/test_aio_low_level.py000066400000000000000000000111501407735772700221750ustar00rootroot00000000000000from dbus_next.aio import MessageBus from dbus_next import Message, MessageType, MessageFlag import pytest @pytest.mark.asyncio async def test_standard_interfaces(): bus = await MessageBus().connect() msg = Message(destination='org.freedesktop.DBus', path='/org/freedesktop/DBus', interface='org.freedesktop.DBus', member='ListNames', serial=bus.next_serial()) reply = await bus.call(msg) assert reply.message_type == MessageType.METHOD_RETURN assert reply.reply_serial == msg.serial assert reply.signature == 'as' assert bus.unique_name in reply.body[0] msg.interface = 'org.freedesktop.DBus.Introspectable' msg.member = 'Introspect' msg.serial = bus.next_serial() reply = await bus.call(msg) assert reply.message_type == MessageType.METHOD_RETURN assert reply.reply_serial == msg.serial assert reply.signature == 's' assert type(reply.body[0]) is str msg.member = 'MemberDoesNotExist' msg.serial = bus.next_serial() reply = await bus.call(msg) assert reply.message_type == MessageType.ERROR assert reply.reply_serial == msg.serial assert reply.error_name assert reply.signature == 's' assert type(reply.body[0]) is str @pytest.mark.asyncio async def test_sending_messages_between_buses(): bus1 = await MessageBus().connect() bus2 = await MessageBus().connect() msg = Message(destination=bus1.unique_name, path='/org/test/path', interface='org.test.iface', member='SomeMember', serial=bus2.next_serial()) def message_handler(sent): if sent.sender == bus2.unique_name and sent.serial == msg.serial: assert sent.path == msg.path assert sent.serial == msg.serial assert sent.interface == msg.interface assert sent.member == msg.member bus1.send(Message.new_method_return(sent, 's', ['got it'])) bus1.remove_message_handler(message_handler) return True bus1.add_message_handler(message_handler) reply = await bus2.call(msg) assert reply.message_type == MessageType.METHOD_RETURN assert reply.sender == bus1.unique_name assert reply.signature == 's' assert reply.body == ['got it'] assert reply.reply_serial == msg.serial def message_handler_error(sent): if sent.sender == bus2.unique_name and sent.serial == msg.serial: assert sent.path == msg.path assert sent.serial == msg.serial assert sent.interface == msg.interface assert sent.member == msg.member bus1.send(Message.new_error(sent, 'org.test.Error', 'throwing an error')) bus1.remove_message_handler(message_handler_error) return True bus1.add_message_handler(message_handler_error) msg.serial = bus2.next_serial() reply = await bus2.call(msg) assert reply.message_type == MessageType.ERROR assert reply.sender == bus1.unique_name assert reply.reply_serial == msg.serial assert reply.error_name == 'org.test.Error' assert reply.signature == 's' assert reply.body == ['throwing an error'] msg.serial = bus2.next_serial() msg.flags = MessageFlag.NO_REPLY_EXPECTED reply = await bus2.call(msg) assert reply is None @pytest.mark.asyncio async def test_sending_signals_between_buses(event_loop): bus1 = await MessageBus().connect() bus2 = await MessageBus().connect() add_match_msg = Message(destination='org.freedesktop.DBus', path='/org/freedesktop/DBus', interface='org.freedesktop.DBus', member='AddMatch', signature='s', body=[f'sender={bus2.unique_name}']) await bus1.call(add_match_msg) async def wait_for_message(): future = event_loop.create_future() def message_handler(signal): if signal.sender == bus2.unique_name: bus1.remove_message_handler(message_handler) future.set_result(signal) bus1.add_message_handler(message_handler) return await future bus2.send( Message.new_signal('/org/test/path', 'org.test.interface', 'SomeSignal', 's', ['a signal'])) signal = await wait_for_message() assert signal.message_type == MessageType.SIGNAL assert signal.path == '/org/test/path' assert signal.interface == 'org.test.interface' assert signal.member == 'SomeSignal' assert signal.signature == 's' assert signal.body == ['a signal'] python-dbus-next-0.2.3/test/test_big_message.py000066400000000000000000000035401407735772700216260ustar00rootroot00000000000000from dbus_next import aio, glib, Message, MessageType from dbus_next.service import ServiceInterface, method from test.util import check_gi_repository, skip_reason_no_gi import pytest has_gi = check_gi_repository() class ExampleInterface(ServiceInterface): def __init__(self): super().__init__('example.interface') @method() def echo_bytes(self, what: 'ay') -> 'ay': return what @pytest.mark.asyncio async def test_aio_big_message(): 'this tests that nonblocking reads and writes actually work for aio' bus1 = await aio.MessageBus().connect() bus2 = await aio.MessageBus().connect() interface = ExampleInterface() bus1.export('/test/path', interface) # two megabytes big_body = [bytes(1000000) * 2] result = await bus2.call( Message(destination=bus1.unique_name, path='/test/path', interface=interface.name, member='echo_bytes', signature='ay', body=big_body)) assert result.message_type == MessageType.METHOD_RETURN, result.body[0] assert result.body[0] == big_body[0] @pytest.mark.skipif(not has_gi, reason=skip_reason_no_gi) def test_glib_big_message(): 'this tests that nonblocking reads and writes actually work for glib' bus1 = glib.MessageBus().connect_sync() bus2 = glib.MessageBus().connect_sync() interface = ExampleInterface() bus1.export('/test/path', interface) # two megabytes big_body = [bytes(1000000) * 2] result = bus2.call_sync( Message(destination=bus1.unique_name, path='/test/path', interface=interface.name, member='echo_bytes', signature='ay', body=big_body)) assert result.message_type == MessageType.METHOD_RETURN, result.body[0] assert result.body[0] == big_body[0] python-dbus-next-0.2.3/test/test_disconnect.py000066400000000000000000000027241407735772700215150ustar00rootroot00000000000000from dbus_next.aio import MessageBus from dbus_next import Message import os import pytest import functools @pytest.mark.asyncio async def test_bus_disconnect_before_reply(event_loop): '''In this test, the bus disconnects before the reply comes in. Make sure the caller receives a reply with the error instead of hanging.''' bus = MessageBus() assert not bus.connected await bus.connect() assert bus.connected ping = bus.call( Message(destination='org.freedesktop.DBus', path='/org/freedesktop/DBus', interface='org.freedesktop.DBus', member='Ping')) event_loop.call_soon(bus.disconnect) with pytest.raises((EOFError, BrokenPipeError)): await ping assert bus._disconnected assert not bus.connected assert (await bus.wait_for_disconnect()) is None @pytest.mark.asyncio async def test_unexpected_disconnect(event_loop): bus = MessageBus() assert not bus.connected await bus.connect() assert bus.connected ping = bus.call( Message(destination='org.freedesktop.DBus', path='/org/freedesktop/DBus', interface='org.freedesktop.DBus', member='Ping')) event_loop.call_soon(functools.partial(os.close, bus._fd)) with pytest.raises(OSError): await ping assert bus._disconnected assert not bus.connected with pytest.raises(OSError): await bus.wait_for_disconnect() python-dbus-next-0.2.3/test/test_fd_passing.py000066400000000000000000000256141407735772700215040ustar00rootroot00000000000000"""This tests the ability to send and receive file descriptors in dbus messages""" from dbus_next.service import ServiceInterface, method, signal, dbus_property from dbus_next.signature import SignatureTree, Variant from dbus_next.aio import MessageBus from dbus_next import Message, MessageType import os import pytest def open_file(): return os.open(os.devnull, os.O_RDONLY) class ExampleInterface(ServiceInterface): def __init__(self, name): super().__init__(name) self.fds = [] @method() def ReturnsFd(self) -> 'h': fd = open_file() self.fds.append(fd) return fd @method() def AcceptsFd(self, fd: 'h'): assert fd != 0 self.fds.append(fd) def get_last_fd(self): return self.fds[-1] def cleanup(self): for fd in self.fds: os.close(fd) self.fds.clear() @signal() def SignalFd(self) -> 'h': fd = open_file() self.fds.append(fd) return fd @dbus_property() def PropFd(self) -> 'h': if not self.fds: fd = open_file() self.fds.append(fd) return self.fds[-1] @PropFd.setter def PropFd(self, fd: 'h'): assert fd self.fds.append(fd) def assert_fds_equal(fd1, fd2): assert fd1 assert fd2 stat1 = os.fstat(fd1) stat2 = os.fstat(fd2) assert stat1.st_dev == stat2.st_dev assert stat1.st_ino == stat2.st_ino assert stat1.st_rdev == stat2.st_rdev @pytest.mark.asyncio async def test_sending_file_descriptor_low_level(): bus1 = await MessageBus(negotiate_unix_fd=True).connect() bus2 = await MessageBus(negotiate_unix_fd=True).connect() fd_before = open_file() fd_after = None msg = Message(destination=bus1.unique_name, path='/org/test/path', interface='org.test.iface', member='SomeMember', body=[0], signature='h', unix_fds=[fd_before]) def message_handler(sent): nonlocal fd_after if sent.sender == bus2.unique_name and sent.serial == msg.serial: assert sent.path == msg.path assert sent.serial == msg.serial assert sent.interface == msg.interface assert sent.member == msg.member assert sent.body == [0] assert len(sent.unix_fds) == 1 fd_after = sent.unix_fds[0] bus1.send(Message.new_method_return(sent, 's', ['got it'])) bus1.remove_message_handler(message_handler) return True bus1.add_message_handler(message_handler) reply = await bus2.call(msg) assert reply.body == ['got it'] assert fd_after is not None assert_fds_equal(fd_before, fd_after) for fd in [fd_before, fd_after]: os.close(fd) for bus in [bus1, bus2]: bus.disconnect() @pytest.mark.asyncio async def test_high_level_service_fd_passing(event_loop): bus1 = await MessageBus(negotiate_unix_fd=True).connect() bus2 = await MessageBus(negotiate_unix_fd=True).connect() interface_name = 'test.interface' interface = ExampleInterface(interface_name) export_path = '/test/path' async def call(member, signature='', body=[], unix_fds=[], iface=interface.name): return await bus2.call( Message(destination=bus1.unique_name, path=export_path, interface=iface, member=member, signature=signature, body=body, unix_fds=unix_fds)) bus1.export(export_path, interface) # test that an fd can be returned by the service reply = await call('ReturnsFd') assert reply.message_type == MessageType.METHOD_RETURN, reply.body assert reply.signature == 'h' assert len(reply.unix_fds) == 1 assert_fds_equal(interface.get_last_fd(), reply.unix_fds[0]) interface.cleanup() os.close(reply.unix_fds[0]) # test that an fd can be sent to the service fd = open_file() reply = await call('AcceptsFd', signature='h', body=[0], unix_fds=[fd]) assert reply.message_type == MessageType.METHOD_RETURN, reply.body assert_fds_equal(interface.get_last_fd(), fd) interface.cleanup() os.close(fd) # signals fut = event_loop.create_future() def fd_listener(msg): if msg.sender == bus1.unique_name and msg.message_type == MessageType.SIGNAL: fut.set_result(msg) reply = await bus2.call( Message(destination='org.freedesktop.DBus', path='/org/freedesktop/DBus', member='AddMatch', signature='s', body=[f"sender='{bus1.unique_name}'"])) assert reply.message_type == MessageType.METHOD_RETURN bus2.add_message_handler(fd_listener) interface.SignalFd() reply = await fut assert len(reply.unix_fds) == 1 assert reply.body == [0] assert_fds_equal(reply.unix_fds[0], interface.get_last_fd()) interface.cleanup() os.close(reply.unix_fds[0]) # properties reply = await call('Get', 'ss', [interface_name, 'PropFd'], iface='org.freedesktop.DBus.Properties') assert reply.message_type == MessageType.METHOD_RETURN, reply.body assert reply.body[0].signature == 'h' assert reply.body[0].value == 0 assert len(reply.unix_fds) == 1 assert_fds_equal(interface.get_last_fd(), reply.unix_fds[0]) interface.cleanup() os.close(reply.unix_fds[0]) fd = open_file() reply = await call('Set', 'ssv', [interface_name, 'PropFd', Variant('h', 0)], iface='org.freedesktop.DBus.Properties', unix_fds=[fd]) assert reply.message_type == MessageType.METHOD_RETURN, reply.body assert_fds_equal(interface.get_last_fd(), fd) interface.cleanup() os.close(fd) reply = await call('GetAll', 's', [interface_name], iface='org.freedesktop.DBus.Properties') assert reply.message_type == MessageType.METHOD_RETURN, reply.body assert reply.body[0]['PropFd'].signature == 'h' assert reply.body[0]['PropFd'].value == 0 assert len(reply.unix_fds) == 1 assert_fds_equal(interface.get_last_fd(), reply.unix_fds[0]) interface.cleanup() os.close(reply.unix_fds[0]) for bus in [bus1, bus2]: bus.disconnect() @pytest.mark.asyncio async def test_sending_file_descriptor_with_proxy(event_loop): name = 'dbus.next.test.service' path = '/test/path' interface_name = 'test.interface' bus = await MessageBus(negotiate_unix_fd=True).connect() interface = ExampleInterface(interface_name) bus.export(path, interface) await bus.request_name(name) intr = await bus.introspect(name, path) proxy = bus.get_proxy_object(name, path, intr) proxy_interface = proxy.get_interface(interface_name) # test fds are replaced correctly in all high level interfaces fd = await proxy_interface.call_returns_fd() assert_fds_equal(interface.get_last_fd(), fd) interface.cleanup() os.close(fd) fd = open_file() await proxy_interface.call_accepts_fd(fd) assert_fds_equal(interface.get_last_fd(), fd) interface.cleanup() os.close(fd) fd = await proxy_interface.get_prop_fd() assert_fds_equal(interface.get_last_fd(), fd) interface.cleanup() os.close(fd) fd = open_file() await proxy_interface.set_prop_fd(fd) assert_fds_equal(interface.get_last_fd(), fd) interface.cleanup() os.close(fd) fut = event_loop.create_future() def on_signal_fd(fd): fut.set_result(fd) proxy_interface.off_signal_fd(on_signal_fd) proxy_interface.on_signal_fd(on_signal_fd) interface.SignalFd() fd = await fut assert_fds_equal(interface.get_last_fd(), fd) interface.cleanup() os.close(fd) @pytest.mark.asyncio @pytest.mark.parametrize( "result, out_signature, expected", [ pytest.param(5, 'h', ([0], [5]), id='Signature: "h"'), pytest.param([5, "foo"], 'hs', ([0, "foo"], [5]), id='Signature: "hs"'), pytest.param([5, 7], 'hh', ([0, 1], [5, 7]), id='Signature: "hh"'), pytest.param([5, 7], 'ah', ([[0, 1]], [5, 7]), id='Signature: "ah"'), pytest.param([9], 'ah', ([[0]], [9]), id='Signature: "ah"'), pytest.param([3], '(h)', ([[0]], [3]), id='Signature: "(h)"'), pytest.param([3, "foo"], '(hs)', ([[0, "foo"]], [3]), id='Signature: "(hs)"'), pytest.param([[7, "foo"], [8, "bar"]], 'a(hs)', ([[[0, "foo"], [1, "bar"]]], [7, 8]), id='Signature: "a(hs)"'), pytest.param({"foo": 3}, 'a{sh}', ([{ "foo": 0 }], [3]), id='Signature: "a{sh}"'), pytest.param({ "foo": 3, "bar": 6 }, 'a{sh}', ([{ "foo": 0, "bar": 1 }], [3, 6]), id='Signature: "a{sh}"'), pytest.param( {"foo": [3, 8]}, 'a{sah}', ([{ "foo": [0, 1] }], [3, 8]), id='Signature: "a{sah}"'), pytest.param({'foo': Variant('t', 100)}, 'a{sv}', ([{ 'foo': Variant('t', 100) }], []), id='Signature: "a{sv}"'), pytest.param(['one', ['two', [Variant('s', 'three')]]], '(s(s(v)))', ([['one', ['two', [Variant('s', 'three')]]]], []), id='Signature: "(s(s(v)))"'), pytest.param(Variant('h', 2), 'v', ([Variant('h', 0)], [2]), id='Variant with: "h"'), pytest.param(Variant('(hh)', [2, 8]), 'v', ([Variant('(hh)', [0, 1])], [2, 8]), id='Variant with: "(hh)"'), pytest.param( Variant('ah', [2, 4]), 'v', ([Variant('ah', [0, 1])], [2, 4]), id='Variant with: "ah"'), pytest.param(Variant('(ss)', ['hello', 'world']), 'v', ([Variant('(ss)', ['hello', 'world'])], []), id='Variant with: "(ss)"'), pytest.param(Variant('v', Variant('t', 100)), 'v', ([Variant('v', Variant('t', 100))], []), id='Variant with: "v"'), pytest.param([ Variant('v', Variant('(ss)', ['hello', 'world'])), { 'foo': Variant('t', 100) }, ['one', ['two', [Variant('s', 'three')]]] ], 'va{sv}(s(s(v)))', ([ Variant('v', Variant('(ss)', ['hello', 'world'])), { 'foo': Variant('t', 100) }, ['one', ['two', [Variant('s', 'three')]]] ], []), id='Variant with: "va{sv}(s(s(v)))"'), ], ) async def test_fn_result_to_body(result, out_signature, expected): out_signature_tree = SignatureTree(out_signature) assert ServiceInterface._fn_result_to_body(result, out_signature_tree) == expected python-dbus-next-0.2.3/test/test_glib_low_level.py000066400000000000000000000115641407735772700223530ustar00rootroot00000000000000from dbus_next.glib import MessageBus from dbus_next import Message, MessageType, MessageFlag from test.util import check_gi_repository, skip_reason_no_gi import pytest has_gi = check_gi_repository() if has_gi: from gi.repository import GLib @pytest.mark.skipif(not has_gi, reason=skip_reason_no_gi) def test_standard_interfaces(): bus = MessageBus().connect_sync() msg = Message(destination='org.freedesktop.DBus', path='/org/freedesktop/DBus', interface='org.freedesktop.DBus', member='ListNames', serial=bus.next_serial()) reply = bus.call_sync(msg) assert reply.message_type == MessageType.METHOD_RETURN assert reply.reply_serial == msg.serial assert reply.signature == 'as' assert bus.unique_name in reply.body[0] msg.interface = 'org.freedesktop.DBus.Introspectable' msg.member = 'Introspect' msg.serial = bus.next_serial() reply = bus.call_sync(msg) assert reply.message_type == MessageType.METHOD_RETURN assert reply.reply_serial == msg.serial assert reply.signature == 's' assert type(reply.body[0]) is str msg.member = 'MemberDoesNotExist' msg.serial = bus.next_serial() reply = bus.call_sync(msg) assert reply.message_type == MessageType.ERROR assert reply.reply_serial == msg.serial assert reply.error_name assert reply.signature == 's' assert type(reply.body[0]) is str @pytest.mark.skipif(not has_gi, reason=skip_reason_no_gi) def test_sending_messages_between_buses(): bus1 = MessageBus().connect_sync() bus2 = MessageBus().connect_sync() msg = Message(destination=bus1.unique_name, path='/org/test/path', interface='org.test.iface', member='SomeMember', serial=bus2.next_serial()) def message_handler(sent): if sent.sender == bus2.unique_name and sent.serial == msg.serial: assert sent.path == msg.path assert sent.serial == msg.serial assert sent.interface == msg.interface assert sent.member == msg.member bus1.send(Message.new_method_return(sent, 's', ['got it'])) bus1.remove_message_handler(message_handler) return True bus1.add_message_handler(message_handler) reply = bus2.call_sync(msg) assert reply.message_type == MessageType.METHOD_RETURN, reply.body[0] assert reply.sender == bus1.unique_name assert reply.signature == 's' assert reply.body == ['got it'] assert reply.reply_serial == msg.serial def message_handler_error(sent): if sent.sender == bus2.unique_name and sent.serial == msg.serial: assert sent.path == msg.path assert sent.serial == msg.serial assert sent.interface == msg.interface assert sent.member == msg.member bus1.send(Message.new_error(sent, 'org.test.Error', 'throwing an error')) bus1.remove_message_handler(message_handler_error) return True bus1.add_message_handler(message_handler_error) msg.serial = bus2.next_serial() reply = bus2.call_sync(msg) assert reply.message_type == MessageType.ERROR assert reply.sender == bus1.unique_name assert reply.reply_serial == msg.serial assert reply.error_name == 'org.test.Error' assert reply.signature == 's' assert reply.body == ['throwing an error'] msg.serial = bus2.next_serial() msg.flags = MessageFlag.NO_REPLY_EXPECTED reply = bus2.call_sync(msg) assert reply is None @pytest.mark.skipif(not has_gi, reason=skip_reason_no_gi) def test_sending_signals_between_buses(): bus1 = MessageBus().connect_sync() bus2 = MessageBus().connect_sync() add_match_msg = Message(destination='org.freedesktop.DBus', path='/org/freedesktop/DBus', interface='org.freedesktop.DBus', member='AddMatch', signature='s', body=[f'sender={bus2.unique_name}']) bus1.call_sync(add_match_msg) main = GLib.MainLoop() def wait_for_message(): ret = None def message_handler(signal): nonlocal ret if signal.sender == bus2.unique_name: ret = signal bus1.remove_message_handler(message_handler) main.quit() bus1.add_message_handler(message_handler) main.run() return ret bus2.send( Message.new_signal('/org/test/path', 'org.test.interface', 'SomeSignal', 's', ['a signal'])) signal = wait_for_message() assert signal.message_type == MessageType.SIGNAL assert signal.path == '/org/test/path' assert signal.interface == 'org.test.interface' assert signal.member == 'SomeSignal' assert signal.signature == 's' assert signal.body == ['a signal'] python-dbus-next-0.2.3/test/test_introspection.py000066400000000000000000000072411407735772700222630ustar00rootroot00000000000000from dbus_next import introspection as intr, ArgDirection, PropertyAccess, SignatureType import os example_data = open(f'{os.path.dirname(__file__)}/data/introspection.xml', 'r').read() def test_example_introspection_from_xml(): node = intr.Node.parse(example_data) assert len(node.interfaces) == 1 interface = node.interfaces[0] assert len(node.nodes) == 2 assert len(interface.methods) == 3 assert len(interface.signals) == 2 assert len(interface.properties) == 1 assert type(node.nodes[0]) is intr.Node assert node.nodes[0].name == 'child_of_sample_object' assert type(node.nodes[1]) is intr.Node assert node.nodes[1].name == 'another_child_of_sample_object' assert interface.name == 'com.example.SampleInterface0' frobate = interface.methods[0] assert type(frobate) is intr.Method assert frobate.name == 'Frobate' assert len(frobate.in_args) == 1 assert len(frobate.out_args) == 2 foo = frobate.in_args[0] assert type(foo) is intr.Arg assert foo.name == 'foo' assert foo.direction == ArgDirection.IN assert foo.signature == 'i' assert type(foo.type) is SignatureType assert foo.type.token == 'i' bar = frobate.out_args[0] assert type(bar) is intr.Arg assert bar.name == 'bar' assert bar.direction == ArgDirection.OUT assert bar.signature == 's' assert type(bar.type) is SignatureType assert bar.type.token == 's' prop = interface.properties[0] assert type(prop) is intr.Property assert prop.name == 'Bar' assert prop.signature == 'y' assert type(prop.type) is SignatureType assert prop.type.token == 'y' assert prop.access == PropertyAccess.WRITE changed = interface.signals[0] assert type(changed) is intr.Signal assert changed.name == 'Changed' assert len(changed.args) == 1 new_value = changed.args[0] assert type(new_value) is intr.Arg assert new_value.name == 'new_value' assert new_value.signature == 'b' def test_example_introspection_to_xml(): node = intr.Node.parse(example_data) tree = node.to_xml() assert tree.tag == 'node' assert tree.attrib.get('name') == '/com/example/sample_object0' assert len(tree) == 3 interface = tree[0] assert interface.tag == 'interface' assert interface.get('name') == 'com.example.SampleInterface0' assert len(interface) == 6 method = interface[0] assert method.tag == 'method' assert method.get('name') == 'Frobate' # TODO annotations assert len(method) == 3 arg = method[0] assert arg.tag == 'arg' assert arg.attrib.get('name') == 'foo' assert arg.attrib.get('type') == 'i' assert arg.attrib.get('direction') == 'in' signal = interface[3] assert signal.tag == 'signal' assert signal.attrib.get('name') == 'Changed' assert len(signal) == 1 arg = signal[0] assert arg.tag == 'arg' assert arg.attrib.get('name') == 'new_value' assert arg.attrib.get('type') == 'b' signal = interface[4] assert signal.tag == 'signal' assert signal.attrib.get('name') == 'ChangedMulti' assert len(signal) == 2 arg = signal[0] assert arg.tag == 'arg' assert arg.attrib.get('name') == 'new_value1' assert arg.attrib.get('type') == 'b' arg = signal[1] assert arg.tag == 'arg' assert arg.attrib.get('name') == 'new_value2' assert arg.attrib.get('type') == 'y' prop = interface[5] assert prop.attrib.get('name') == 'Bar' assert prop.attrib.get('type') == 'y' assert prop.attrib.get('access') == 'write' def test_default_interfaces(): # just make sure it doesn't throw default = intr.Node.default() assert type(default) is intr.Node python-dbus-next-0.2.3/test/test_marshaller.py000066400000000000000000000063571407735772700215240ustar00rootroot00000000000000from dbus_next._private.unmarshaller import Unmarshaller from dbus_next import Message, Variant, SignatureTree import json import os import io def print_buf(buf): i = 0 while True: p = buf[i:i + 8] if not p: break print(p) i += 8 # these messages have been verified with another library table = json.load(open(os.path.dirname(__file__) + '/data/messages.json')) # variants are an object in the json def replace_variants(type_, item): if type_.token == 'v' and type(item) is not Variant: item = Variant(item['signature'], replace_variants(SignatureTree(item['signature']).types[0], item['value'])) elif type_.token == 'a': for i, item_child in enumerate(item): if type_.children[0].token == '{': for k, v in item.items(): item[k] = replace_variants(type_.children[0].children[1], v) else: item[i] = replace_variants(type_.children[0], item_child) elif type_.token == '(': for i, item_child in enumerate(item): if type_.children[0].token == '{': assert False else: item[i] = replace_variants(type_.children[i], item_child) return item def json_dump(what): def dumper(obj): try: return obj.toJSON() except Exception: return obj.__dict__ return json.dumps(what, default=dumper, indent=2) def test_marshalling_with_table(): for item in table: message = Message(**item['message']) body = [] for i, type_ in enumerate(message.signature_tree.types): body.append(replace_variants(type_, message.body[i])) message.body = body buf = message._marshall() data = bytes.fromhex(item['data']) if buf != data: print('message:') print(json_dump(item['message'])) print('') print('mine:') print_buf(bytes(buf)) print('') print('theirs:') print_buf(data) assert buf == data def test_unmarshalling_with_table(): for item in table: stream = io.BytesIO(bytes.fromhex(item['data'])) unmarshaller = Unmarshaller(stream) try: unmarshaller.unmarshall() except Exception as e: print('message failed to unmarshall:') print(json_dump(item['message'])) raise e message = Message(**item['message']) body = [] for i, type_ in enumerate(message.signature_tree.types): body.append(replace_variants(type_, message.body[i])) message.body = body for attr in [ 'body', 'signature', 'message_type', 'destination', 'path', 'interface', 'member', 'flags', 'serial' ]: assert getattr(unmarshaller.message, attr) == getattr(message, attr), f'attr doesnt match: {attr}' def test_ay_buffer(): body = [bytes(10000)] msg = Message(path='/test', member='test', signature='ay', body=body) marshalled = msg._marshall() unmarshalled_msg = Unmarshaller(io.BytesIO(marshalled)).unmarshall() assert unmarshalled_msg.body[0] == body[0] python-dbus-next-0.2.3/test/test_request_name.py000066400000000000000000000042641407735772700220550ustar00rootroot00000000000000from dbus_next import aio, glib, Message, MessageType, NameFlag, RequestNameReply, ReleaseNameReply from test.util import check_gi_repository, skip_reason_no_gi import pytest has_gi = check_gi_repository() @pytest.mark.asyncio async def test_name_requests(): test_name = 'aio.test.request.name' bus1 = await aio.MessageBus().connect() bus2 = await aio.MessageBus().connect() async def get_name_owner(name): reply = await bus1.call( Message(destination='org.freedesktop.DBus', path='/org/freedesktop/DBus', interface='org.freedesktop.DBus', member='GetNameOwner', signature='s', body=[name])) assert reply.message_type == MessageType.METHOD_RETURN return reply.body[0] reply = await bus1.request_name(test_name) assert reply == RequestNameReply.PRIMARY_OWNER reply = await bus1.request_name(test_name) assert reply == RequestNameReply.ALREADY_OWNER reply = await bus2.request_name(test_name, NameFlag.ALLOW_REPLACEMENT) assert reply == RequestNameReply.IN_QUEUE reply = await bus1.release_name(test_name) assert reply == ReleaseNameReply.RELEASED reply = await bus1.release_name('name.doesnt.exist') assert reply == ReleaseNameReply.NON_EXISTENT reply = await bus1.release_name(test_name) assert reply == ReleaseNameReply.NOT_OWNER new_owner = await get_name_owner(test_name) assert new_owner == bus2.unique_name reply = await bus1.request_name(test_name, NameFlag.DO_NOT_QUEUE) assert reply == RequestNameReply.EXISTS reply = await bus1.request_name(test_name, NameFlag.DO_NOT_QUEUE | NameFlag.REPLACE_EXISTING) assert reply == RequestNameReply.PRIMARY_OWNER bus1.disconnect() bus2.disconnect() @pytest.mark.skipif(not has_gi, reason=skip_reason_no_gi) def test_request_name_glib(): test_name = 'glib.test.request.name' bus = glib.MessageBus().connect_sync() reply = bus.request_name_sync(test_name) assert reply == RequestNameReply.PRIMARY_OWNER reply = bus.release_name_sync(test_name) assert reply == ReleaseNameReply.RELEASED bus.disconnect() python-dbus-next-0.2.3/test/test_signature.py000066400000000000000000000144401407735772700213630ustar00rootroot00000000000000from dbus_next import SignatureTree, SignatureBodyMismatchError, Variant from dbus_next._private.util import signature_contains_type import pytest def assert_simple_type(signature, type_): assert type_.token == signature assert type_.signature == signature assert len(type_.children) == 0 def test_simple(): tree = SignatureTree('s') assert len(tree.types) == 1 assert_simple_type('s', tree.types[0]) def test_multiple_simple(): tree = SignatureTree('sss') assert len(tree.types) == 3 for i in range(0, 3): assert_simple_type('s', tree.types[i]) def test_array(): tree = SignatureTree('as') assert len(tree.types) == 1 child = tree.types[0] assert child.signature == 'as' assert child.token == 'a' assert len(child.children) == 1 assert_simple_type('s', child.children[0]) def test_array_multiple(): tree = SignatureTree('asasass') assert len(tree.types) == 4 assert_simple_type('s', tree.types[3]) for i in range(0, 3): array_child = tree.types[i] assert array_child.token == 'a' assert array_child.signature == 'as' assert len(array_child.children) == 1 assert_simple_type('s', array_child.children[0]) def test_array_nested(): tree = SignatureTree('aas') assert len(tree.types) == 1 child = tree.types[0] assert child.token == 'a' assert child.signature == 'aas' assert len(child.children) == 1 nested_child = child.children[0] assert nested_child.token == 'a' assert nested_child.signature == 'as' assert len(nested_child.children) == 1 assert_simple_type('s', nested_child.children[0]) def test_simple_struct(): tree = SignatureTree('(sss)') assert len(tree.types) == 1 child = tree.types[0] assert child.signature == '(sss)' assert len(child.children) == 3 for i in range(0, 3): assert_simple_type('s', child.children[i]) def test_nested_struct(): tree = SignatureTree('(s(s(s)))') assert len(tree.types) == 1 child = tree.types[0] assert child.signature == '(s(s(s)))' assert child.token == '(' assert len(child.children) == 2 assert_simple_type('s', child.children[0]) first_nested = child.children[1] assert first_nested.token == '(' assert first_nested.signature == '(s(s))' assert len(first_nested.children) == 2 assert_simple_type('s', first_nested.children[0]) second_nested = first_nested.children[1] assert second_nested.token == '(' assert second_nested.signature == '(s)' assert len(second_nested.children) == 1 assert_simple_type('s', second_nested.children[0]) def test_struct_multiple(): tree = SignatureTree('(s)(s)(s)') assert len(tree.types) == 3 for i in range(0, 3): child = tree.types[0] assert child.token == '(' assert child.signature == '(s)' assert len(child.children) == 1 assert_simple_type('s', child.children[0]) def test_array_of_structs(): tree = SignatureTree('a(ss)') assert len(tree.types) == 1 child = tree.types[0] assert child.token == 'a' assert child.signature == 'a(ss)' assert len(child.children) == 1 struct_child = child.children[0] assert struct_child.token == '(' assert struct_child.signature == '(ss)' assert len(struct_child.children) == 2 for i in range(0, 2): assert_simple_type('s', struct_child.children[i]) def test_dict_simple(): tree = SignatureTree('a{ss}') assert len(tree.types) == 1 child = tree.types[0] assert child.signature == 'a{ss}' assert child.token == 'a' assert len(child.children) == 1 dict_child = child.children[0] assert dict_child.token == '{' assert dict_child.signature == '{ss}' assert len(dict_child.children) == 2 assert_simple_type('s', dict_child.children[0]) assert_simple_type('s', dict_child.children[1]) def test_dict_of_structs(): tree = SignatureTree('a{s(ss)}') assert len(tree.types) == 1 child = tree.types[0] assert child.token == 'a' assert child.signature == 'a{s(ss)}' assert len(child.children) == 1 dict_child = child.children[0] assert dict_child.token == '{' assert dict_child.signature == '{s(ss)}' assert len(dict_child.children) == 2 assert_simple_type('s', dict_child.children[0]) struct_child = dict_child.children[1] assert struct_child.token == '(' assert struct_child.signature == '(ss)' assert len(struct_child.children) == 2 for i in range(0, 2): assert_simple_type('s', struct_child.children[i]) def test_contains_type(): tree = SignatureTree('h') assert signature_contains_type(tree, [0], 'h') assert not signature_contains_type(tree, [0], 'u') tree = SignatureTree('ah') assert signature_contains_type(tree, [[0]], 'h') assert signature_contains_type(tree, [[0]], 'a') assert not signature_contains_type(tree, [[0]], 'u') tree = SignatureTree('av') body = [[Variant('u', 0), Variant('i', 0), Variant('x', 0), Variant('v', Variant('s', 'hi'))]] assert signature_contains_type(tree, body, 'u') assert signature_contains_type(tree, body, 'x') assert signature_contains_type(tree, body, 'v') assert signature_contains_type(tree, body, 's') assert not signature_contains_type(tree, body, 'o') tree = SignatureTree('a{sv}') body = { 'foo': Variant('h', 0), 'bar': Variant('i', 0), 'bat': Variant('x', 0), 'baz': Variant('v', Variant('o', '/hi')) } for expected in 'hixvso': assert signature_contains_type(tree, [body], expected) assert not signature_contains_type(tree, [body], 'b') def test_invalid_variants(): tree = SignatureTree('a{sa{sv}}') s_con = { 'type': '802-11-wireless', 'uuid': '1234', 'id': 'SSID', } s_wifi = { 'ssid': 'SSID', 'mode': 'infrastructure', 'hidden': True, } s_wsec = { 'key-mgmt': 'wpa-psk', 'auth-alg': 'open', 'psk': 'PASSWORD', } s_ip4 = {'method': 'auto'} s_ip6 = {'method': 'auto'} con = { 'connection': s_con, '802-11-wireless': s_wifi, '802-11-wireless-security': s_wsec, 'ipv4': s_ip4, 'ipv6': s_ip6 } with pytest.raises(SignatureBodyMismatchError): tree.verify([con]) python-dbus-next-0.2.3/test/test_tcp_address.py000066400000000000000000000041561407735772700216600ustar00rootroot00000000000000from dbus_next.aio import MessageBus from dbus_next import Message from dbus_next._private.address import parse_address import asyncio import pytest import os @pytest.mark.asyncio async def test_tcp_connection_with_forwarding(event_loop): closables = [] host = '127.0.0.1' port = '55556' addr_info = parse_address(os.environ.get('DBUS_SESSION_BUS_ADDRESS')) assert addr_info assert 'abstract' in addr_info[0][1] path = f'\0{addr_info[0][1]["abstract"]}' async def handle_connection(tcp_reader, tcp_writer): unix_reader, unix_writer = await asyncio.open_unix_connection(path) closables.append(tcp_writer) closables.append(unix_writer) async def handle_read(): while True: data = await tcp_reader.read(1) if not data: break unix_writer.write(data) async def handle_write(): while True: data = await unix_reader.read(1) if not data: break tcp_writer.write(data) asyncio.run_coroutine_threadsafe(handle_read(), event_loop) asyncio.run_coroutine_threadsafe(handle_write(), event_loop) server = await asyncio.start_server(handle_connection, host, port) closables.append(server) bus = await MessageBus(bus_address=f'tcp:host={host},port={port}').connect() # basic tests to see if it works result = await bus.call( Message(destination='org.freedesktop.DBus', path='/org/freedesktop/DBus', interface='org.freedesktop.DBus.Peer', member='Ping')) assert result intr = await bus.introspect('org.freedesktop.DBus', '/org/freedesktop/DBus') obj = bus.get_proxy_object('org.freedesktop.DBus', '/org/freedesktop/DBus', intr) iface = obj.get_interface('org.freedesktop.DBus.Peer') await iface.call_ping() assert bus._sock.getpeername()[0] == host assert bus._sock.getsockname()[0] == host assert bus._sock.gettimeout() == 0 assert bus._stream.closed is False for c in closables: c.close() python-dbus-next-0.2.3/test/test_validators.py000066400000000000000000000040111407735772700215230ustar00rootroot00000000000000from dbus_next import (is_bus_name_valid, is_object_path_valid, is_interface_name_valid, is_member_name_valid) def test_object_path_validator(): valid_paths = ['/', '/foo', '/foo/bar', '/foo/bar/bat'] invalid_paths = [ None, {}, '', 'foo', 'foo/bar', '/foo/bar/', '/$/foo/bar', '/foo//bar', '/foo$bar/baz' ] for path in valid_paths: assert is_object_path_valid(path), f'path should be valid: "{path}"' for path in invalid_paths: assert not is_object_path_valid(path), f'path should be invalid: "{path}"' def test_bus_name_validator(): valid_names = [ 'foo.bar', 'foo.bar.bat', '_foo._bar', 'foo.bar69', 'foo.bar-69', 'org.mpris.MediaPlayer2.google-play-desktop-player' ] invalid_names = [ None, {}, '', '5foo.bar', 'foo.6bar', '.foo.bar', 'bar..baz', '$foo.bar', 'foo$.ba$r' ] for name in valid_names: assert is_bus_name_valid(name), f'bus name should be valid: "{name}"' for name in invalid_names: assert not is_bus_name_valid(name), f'bus name should be invalid: "{name}"' def test_interface_name_validator(): valid_names = ['foo.bar', 'foo.bar.bat', '_foo._bar', 'foo.bar69'] invalid_names = [ None, {}, '', '5foo.bar', 'foo.6bar', '.foo.bar', 'bar..baz', '$foo.bar', 'foo$.ba$r', 'org.mpris.MediaPlayer2.google-play-desktop-player' ] for name in valid_names: assert is_interface_name_valid(name), f'interface name should be valid: "{name}"' for name in invalid_names: assert not is_interface_name_valid(name), f'interface name should be invalid: "{name}"' def test_member_name_validator(): valid_members = ['foo', 'FooBar', 'Bat_Baz69'] invalid_members = [None, {}, '', 'foo.bar', '5foo', 'foo$bar'] for member in valid_members: assert is_member_name_valid(member), f'member name should be valid: "{member}"' for member in invalid_members: assert not is_member_name_valid(member), f'member name should be invalid: "{member}"' python-dbus-next-0.2.3/test/util.py000066400000000000000000000005131407735772700172740ustar00rootroot00000000000000_has_gi = None skip_reason_no_gi = 'glib tests require python3-gi' def check_gi_repository(): global _has_gi if _has_gi is not None: return _has_gi try: from gi.repository import GLib _has_gi = True return _has_gi except ImportError: _has_gi = False return _has_gi