chore: initial port

This commit is contained in:
J. Nick Koston 2022-09-09 08:43:26 -05:00
parent 169581f691
commit 495bfac17f
84 changed files with 10876 additions and 17 deletions

20
docs/source/Makefile Normal file
View File

@ -0,0 +1,20 @@
# 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)

0
docs/source/_static/.gitignore vendored Normal file
View File

0
docs/source/_templates/.gitignore vendored Normal file
View File

View File

@ -0,0 +1,9 @@
Authentication
==============
Classes for the DBus `authentication protocol <https://dbus.freedesktop.org/doc/dbus-specification.html#auth-protocol>`_ for us with :class:`MessageBus <dbus_next.message_bus.BaseMessageBus>` implementations.
.. autoclass:: dbus_next.auth.Authenticator
.. autoclass:: dbus_next.auth.AuthExternal
.. autoclass:: dbus_next.auth.AuthAnnonymous

View File

@ -1,8 +1,9 @@
#
# Configuration file for the Sphinx documentation builder.
#
# This file only contains a selection of the most common options. For a full
# list see the documentation:
# https://www.sphinx-doc.org/en/master/usage/configuration.html
# 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 --------------------------------------------------------------
@ -10,47 +11,147 @@
# 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('.'))
import os
import sys
sys.path.insert(0, os.path.abspath(os.path.dirname(__file__) + "/.."))
from dbus_next.__version__ import __author__, __copyright__, __title__, __version__
_project_slug = __title__.replace("_", "-")
# -- Project information -----------------------------------------------------
project = "dbus-fast"
copyright = "2020, Bluetooth Devices Authors"
author = "Bluetooth Devices Authors"
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 = [
"myst_parser",
"sphinx.ext.autodoc",
"sphinx.ext.githubpages",
"sphinxcontrib.asyncio",
"sphinxcontrib.fulltoc",
]
# The suffix of source filenames.
source_suffix = [".rst", ".md"]
# 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 = []
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 = "sphinx_rtd_theme"
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 -------------------------------------------------

39
docs/source/constants.rst Normal file
View File

@ -0,0 +1,39 @@
Constants
=========
.. 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:

19
docs/source/errors.rst Normal file
View File

@ -0,0 +1,19 @@
Errors
======
.. 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

View File

@ -0,0 +1,7 @@
aio.ProxyInterface
==================
.. autoclass:: dbus_next.aio.ProxyInterface
:members:
:undoc-members:
:show-inheritance:

View File

@ -0,0 +1,7 @@
aio.ProxyObject
===============
.. autoclass:: dbus_next.aio.ProxyObject
:members:
:undoc-members:
:show-inheritance:

View File

@ -0,0 +1,6 @@
BaseProxyInterface
==================
.. autoclass:: dbus_next.proxy_object.BaseProxyInterface
:members:
:undoc-members:

View File

@ -0,0 +1,6 @@
BaseProxyObject
===============
.. autoclass:: dbus_next.proxy_object.BaseProxyObject
:members:
:undoc-members:

View File

@ -0,0 +1,7 @@
glib.ProxyInterface
===================
.. autoclass:: dbus_next.glib.ProxyInterface
:members:
:undoc-members:
:show-inheritance:

View File

@ -0,0 +1,7 @@
glib.ProxyObject
================
.. autoclass:: dbus_next.glib.ProxyObject
:members:
:undoc-members:
:show-inheritance:

View File

@ -0,0 +1,100 @@
The 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 <https://dbus.freedesktop.org/doc/dbus-specification.html#introspection-format>`_ which is exposed over the standard `org.freedesktop.DBus.Introspectable <https://dbus.freedesktop.org/doc/dbus-specification.html#standard-interfaces-introspectable>`_ interface. Calling the ``Introspect`` at a particular object path may return XML data similar to this:
.. code-block:: xml
<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN"
"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
<node name="/com/example/sample_object0">
<interface name="com.example.SampleInterface0">
<method name="Frobate">
<arg name="foo" type="i" direction="in"/>
<arg name="bar" type="s" direction="out"/>
<arg name="baz" type="a{us}" direction="out"/>
</method>
<method name="Bazify">
<arg name="bar" type="(iiu)" direction="in"/>
<arg name="bar" type="v" direction="out"/>
</method>
<method name="Mogrify">
<arg name="bar" type="(iiav)" direction="in"/>
</method>
<signal name="Changed">
<arg name="new_value" type="b"/>
</signal>
<property name="Bar" type="y" access="readwrite"/>
</interface>
<node name="child_of_sample_object"/>
<node name="another_child_of_sample_object"/>
</node>
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 <dbus_next.proxy_object.BaseProxyObject>`. The interfaces contained in the nodes are represented by a :class:`ProxyInterface <dbus_next.proxy_object.BaseProxyInterface>`. The proxy interface exposes the methods, signals, and properties specified by the interface definition.
The proxy object is obtained by the :class:`MessageBus <dbus_next.message_bus.BaseMessageBus>` through the :func:`get_proxy_object() <dbus_next.message_bus.BaseMessageBus.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() <dbus_next.message_bus.BaseMessageBus.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() <dbus_next.proxy_object.BaseProxyObject.get_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 <dbus_next.aio.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()

View File

@ -0,0 +1,93 @@
The 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 <dbus_next.message_bus.BaseMessageBus>`, 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() <dbus_next.message_bus.BaseMessageBus.request_name>` so users have a predictable name to use to send messages your client.
Services are defined by subclassing :class:`ServiceInterface <dbus_next.service.ServiceInterface>` and definining members as methods on the class with the decorator methods :func:`@method() <dbus_next.service.method>`, :func:`@dbus_property() <dbus_next.service.dbus_property>`, and :func:`@signal() <dbus_next.service.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 </type-system/index.html>`_ 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 <dbus_next.DBusError>` to return a detailed error to the client if something goes wrong.
After the service interface is defined, call :func:`MessageBus.export() <dbus_next.message_bus.BaseMessageBus.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())

View File

@ -0,0 +1,12 @@
ServiceInterface
================
.. autoclass:: dbus_next.service.ServiceInterface
:members:
:undoc-members:
.. autodecorator:: dbus_next.service.dbus_property
.. autodecorator:: dbus_next.service.method
.. autodecorator:: dbus_next.service.signal

72
docs/source/index.rst Normal file
View File

@ -0,0 +1,72 @@
Python 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 <https://www.freedesktop.org/wiki/Software/dbus/>`_ 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 <high-level-client/index.html>`_ - Communicate with an existing interface exported on the bus by another client through a proxy object.
- `The High Level Service <high-level-service/index.html>`_ - Export a service interface for your application other clients can connect to for interaction with your application at runtime.
- `The Low Level Interface <low-level-interface/index.html>`_ - 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 <https://pypi.org/project/dbus-next/>`_.
.. code-block:: bash
pip3 install dbus-next
Contributing
++++++++++++
Development for this library happens on `Github <https://github.com/altdesktop/python-dbus-next>`_. Report bugs or request features there. Contributions are welcome.
License
++++++++
This library is available under an `MIT License <https://github.com/altdesktop/python-dbus-next/blob/master/LICENSE>`_.
© 2019, Tony Crisci
Indices and tables
==================
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`

View File

@ -0,0 +1,26 @@
Introspection
=============
.. 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:

View File

@ -0,0 +1,100 @@
The Low Level Interface
=======================
.. toctree::
:maxdepth: 2
message
The low-level interface allows you to work with messages directly through the :class:`MessageBus <dbus_next.message_bus.BaseMessageBus>` with the :class:`Message <dbus_next.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 <dbus_next.message_bus.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 <dbus_next.Message>`
- :func:`MessageBus.send() <dbus_next.message_bus.BaseMessageBus.send>`
- :func:`MessageBus.add_message_handler() <dbus_next.message_bus.BaseMessageBus.add_message_handler>`
- :func:`MessageBus.remove_message_handler() <dbus_next.message_bus.BaseMessageBus.remove_message_handler>`
- :func:`MessageBus.next_serial() <dbus_next.message_bus.BaseMessageBus.next_serial>`
- :func:`aio.MessageBus.call() <dbus_next.aio.MessageBus.call>`
- :func:`glib.MessageBus.call() <dbus_next.glib.MessageBus.call>`
- :func:`glib.MessageBus.call_sync() <dbus_next.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 <https://dbus.freedesktop.org/doc/dbus-specification.html#message-bus-routing-match-rules>`_ 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)

View File

@ -0,0 +1,6 @@
Message
=======
.. autoclass:: dbus_next.Message
:members:
:undoc-members:

View File

@ -0,0 +1,7 @@
aio.MessageBus
==============
.. autoclass:: dbus_next.aio.MessageBus
:members:
:inherited-members:
:show-inheritance:

View File

@ -0,0 +1,5 @@
BaseMessageBus
==============
.. autoclass:: dbus_next.message_bus.BaseMessageBus
:members:

View File

@ -0,0 +1,7 @@
glib.MessageBus
===============
.. autoclass:: dbus_next.glib.MessageBus
:members:
:inherited-members:
:show-inheritance:

View File

@ -0,0 +1,17 @@
The 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 <dbus_next.aio.MessageBus>` if you are using an asyncio main loop. Use :class:`glib.MessageBus <dbus_next.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.

View File

@ -0,0 +1,102 @@
The 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 <https://dbus.freedesktop.org/doc/dbus-specification.html#introspection-format>`__.
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 <dbus_next.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 <dbus_next.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 <https://dbus.freedesktop.org/doc/dbus-specification.html#type-system>`__.

View File

@ -0,0 +1,6 @@
SignatureTree
=============
.. autoclass:: dbus_next.SignatureTree
:members:
:undoc-members:

View File

@ -0,0 +1,7 @@
SignatureType
==============
.. autoclass:: dbus_next.SignatureType
:members:
:undoc-members:
:exclude-members: signature

View File

@ -0,0 +1,6 @@
Variant
=======
.. autoclass:: dbus_next.Variant
:members:
:undoc-members:

View File

@ -0,0 +1,11 @@
Validators
==========
.. 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

34
examples/aio-list-names.py Executable file
View File

@ -0,0 +1,34 @@
#!/usr/bin/env python3
import os
import sys
sys.path.append(os.path.abspath(os.path.dirname(__file__) + "/.."))
import asyncio
import json
from dbus_next import Message, MessageType
from dbus_next.aio import MessageBus
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())

View File

@ -0,0 +1,37 @@
#!/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 os
import sys
sys.path.append(os.path.abspath(os.path.dirname(__file__) + "/.."))
import asyncio
from dbus_next.aio import MessageBus
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())

138
examples/dbus-next-send.py Executable file
View File

@ -0,0 +1,138 @@
#!/usr/bin/env python3
import os
import sys
sys.path.append(os.path.abspath(os.path.dirname(__file__) + "/.."))
import asyncio
import json
from argparse import OPTIONAL, ArgumentParser
from dbus_next import BusType, Message, MessageType, Variant
from dbus_next.aio import MessageBus
from dbus_next.validators import (
is_bus_name_valid,
is_interface_name_valid,
is_member_name_valid,
is_object_path_valid,
)
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())

67
examples/example-service.py Executable file
View File

@ -0,0 +1,67 @@
#!/usr/bin/env python3
import os
import sys
sys.path.append(os.path.abspath(os.path.dirname(__file__) + "/.."))
import asyncio
from dbus_next import Variant
from dbus_next.aio.message_bus import MessageBus
from dbus_next.service import ServiceInterface, dbus_property, method, signal
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())

38
examples/glib-list-names.py Executable file
View File

@ -0,0 +1,38 @@
#!/usr/bin/env python3
import os
import sys
sys.path.append(os.path.abspath(os.path.dirname(__file__) + "/.."))
import json
import signal
from dbus_next import Message
from dbus_next.glib import MessageBus
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()

48
examples/mpris.py Executable file
View File

@ -0,0 +1,48 @@
#!/usr/bin/env python3
import os
import sys
sys.path.append(os.path.abspath(os.path.dirname(__file__) + "/.."))
import asyncio
from dbus_next.aio import MessageBus
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())

View File

@ -1 +1,39 @@
__version__ = "1.0.0"
from . import aio, glib, introspection, message_bus, proxy_object, service
from .constants import (
ArgDirection,
BusType,
ErrorType,
MessageFlag,
MessageType,
NameFlag,
PropertyAccess,
ReleaseNameReply,
RequestNameReply,
)
from .errors import (
AuthError,
DBusError,
InterfaceNotFoundError,
InvalidAddressError,
InvalidBusNameError,
InvalidInterfaceNameError,
InvalidIntrospectionError,
InvalidMemberNameError,
InvalidMessageError,
InvalidObjectPathError,
InvalidSignatureError,
SignalDisabledError,
SignatureBodyMismatchError,
)
from .message import Message
from .signature import SignatureTree, SignatureType, Variant
from .validators import (
assert_bus_name_valid,
assert_interface_name_valid,
assert_member_name_valid,
assert_object_path_valid,
is_bus_name_valid,
is_interface_name_valid,
is_member_name_valid,
is_object_path_valid,
)

View File

@ -0,0 +1,8 @@
__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"

View File

View File

@ -0,0 +1,106 @@
import os
import re
from urllib.parse import unquote
from ..constants import BusType
from ..errors import InvalidAddressError
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}")

View File

@ -0,0 +1,18 @@
from 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

View File

@ -0,0 +1,161 @@
from struct import pack
from ..signature import SignatureTree
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("<h", int16))
return written + 2
def write_uint16(self, uint16, _=None):
written = self.align(2)
self.buffer.extend(pack("<H", uint16))
return written + 2
def write_int32(self, int32, _):
written = self.align(4)
self.buffer.extend(pack("<i", int32))
return written + 4
def write_uint32(self, uint32, _=None):
written = self.align(4)
self.buffer.extend(pack("<I", uint32))
return written + 4
def write_int64(self, int64, _=None):
written = self.align(8)
self.buffer.extend(pack("<q", int64))
return written + 8
def write_uint64(self, uint64, _=None):
written = self.align(8)
self.buffer.extend(pack("<Q", uint64))
return written + 8
def write_double(self, double, _=None):
written = self.align(8)
self.buffer.extend(pack("<d", double))
return written + 8
def write_signature(self, signature, _=None):
signature = signature.encode()
signature_len = len(signature)
self.buffer.append(signature_len)
self.buffer.extend(signature)
self.buffer.append(0)
return signature_len + 2
def write_string(self, value, _=None):
value = value.encode()
value_len = len(value)
written = self.write_uint32(value_len)
self.buffer.extend(value)
written += value_len
self.buffer.append(0)
written += 1
return written
def write_variant(self, variant, _=None):
written = self.write_signature(variant.signature)
written += self.write_single(variant.type, variant.value)
return written
def write_array(self, array, type_):
# TODO max array size is 64MiB (67108864 bytes)
written = self.align(4)
# length placeholder
offset = len(self.buffer)
written += self.write_uint32(0)
child_type = type_.children[0]
if child_type.token in "xtd{(":
# the first alignment is not included in array size
written += self.align(8)
array_len = 0
if child_type.token == "{":
for key, value in array.items():
array_len += self.write_dict_entry([key, value], child_type)
elif child_type.token == "y":
array_len = len(array)
self.buffer.extend(array)
else:
for value in array:
array_len += self.write_single(child_type, value)
array_len_packed = pack("<I", array_len)
for i in range(offset, offset + 4):
self.buffer[i] = array_len_packed[i - offset]
return written + array_len
def write_struct(self, array, type_):
written = self.align(8)
for i, value in enumerate(array):
written += self.write_single(type_.children[i], value)
return written
def write_dict_entry(self, dict_entry, type_):
written = self.align(8)
written += self.write_single(type_.children[0], dict_entry[0])
written += self.write_single(type_.children[1], dict_entry[1])
return written
def write_single(self, type_, body):
t = type_.token
if t not in self.writers:
raise NotImplementedError(f'type isnt implemented yet: "{t}"')
return self.writers[t](body, type_)
def marshall(self):
self.buffer.clear()
for i, type_ in enumerate(self.signature_tree.types):
self.write_single(type_, self.body[i])
return self.buffer

View File

@ -0,0 +1,315 @@
import array
import socket
from codecs import decode
from struct import unpack_from
from ..constants import MessageFlag, MessageType
from ..errors import InvalidMessageError
from ..message import Message
from ..signature import SignatureTree, Variant
from .constants import BIG_ENDIAN, LITTLE_ENDIAN, PROTOCOL_VERSION, HeaderField
MAX_UNIX_FDS = 16
class MarshallerStreamEndError(Exception):
pass
class Unmarshaller:
def __init__(self, stream, sock=None):
self.unix_fds = []
self.buf = bytearray()
self.offset = 0
self.stream = stream
self.sock = sock
self.endian = None
self.message = None
self.readers = {
"y": self.read_byte,
"b": self.read_boolean,
"n": self.read_int16,
"q": self.read_uint16,
"i": self.read_int32,
"u": self.read_uint32,
"x": self.read_int64,
"t": self.read_uint64,
"d": self.read_double,
"h": self.read_uint32,
"o": self.read_string,
"s": self.read_string,
"g": self.read_signature,
"a": self.read_array,
"(": self.read_struct,
"{": self.read_dict_entry,
"v": self.read_variant,
}
def read(self, n, prefetch=False):
"""
Read from underlying socket into buffer and advance offset accordingly.
:arg n:
Number of bytes to read. If not enough bytes are available in the
buffer, read more from it.
:arg prefetch:
Do not update current offset after reading.
:returns:
Previous offset (before reading). To get the actual read bytes,
use the returned value and self.buf.
"""
def read_sock(length):
"""reads from the socket, storing any fds sent and handling errors
from the read itself"""
if self.sock is not None:
unix_fd_list = array.array("i")
try:
msg, ancdata, *_ = self.sock.recvmsg(
length, socket.CMSG_LEN(MAX_UNIX_FDS * unix_fd_list.itemsize)
)
except BlockingIOError:
raise MarshallerStreamEndError()
for level, type_, data in ancdata:
if not (level == socket.SOL_SOCKET and type_ == socket.SCM_RIGHTS):
continue
unix_fd_list.frombytes(
data[: len(data) - (len(data) % unix_fd_list.itemsize)]
)
self.unix_fds.extend(list(unix_fd_list))
return msg
else:
return self.stream.read(length)
# store previously read data in a buffer so we can resume on socket
# interruptions
missing_bytes = n - (len(self.buf) - self.offset)
if missing_bytes > 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

View File

@ -0,0 +1,172 @@
import ast
import inspect
from typing import Any, List, Union
from ..signature import SignatureTree, Variant
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)

View File

@ -0,0 +1,2 @@
from .message_bus import MessageBus
from .proxy_object import ProxyInterface, ProxyObject

View File

@ -0,0 +1,463 @@
import array
import asyncio
import logging
import socket
from asyncio import Queue
from copy import copy
from typing import Optional
from .. import introspection as intr
from .._private.unmarshaller import Unmarshaller
from ..auth import Authenticator, AuthExternal
from ..constants import (
BusType,
MessageFlag,
MessageType,
NameFlag,
ReleaseNameReply,
RequestNameReply,
)
from ..errors import AuthError
from ..message import Message
from ..message_bus import BaseMessageBus
from ..service import ServiceInterface
from .proxy_object import ProxyObject
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() <dbus_next.aio.MessageBus.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 <dbus_next.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 <dbus_next.auth.AuthExternal>`.
:type auth: :class:`Authenticator <dbus_next.auth.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 <dbus_next.aio.MessageBus>`
:raises:
- :class:`AuthError <dbus_next.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 <dbus_next.introspection.Node>`
:raises:
- :class:`InvalidObjectPathError <dbus_next.InvalidObjectPathError>` \
- If the given object path is not valid.
- :class:`InvalidBusNameError <dbus_next.InvalidBusNameError>` - If \
the given bus name is not valid.
- :class:`DBusError <dbus_next.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 <dbus_next.NameFlag>`
:returns: The reply to the name request.
:rtype: :class:`RequestNameReply <dbus_next.RequestNameReply>`
:raises:
- :class:`InvalidBusNameError <dbus_next.InvalidBusNameError>` - If \
the given bus name is not valid.
- :class:`DBusError <dbus_next.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 <dbus_next.ReleaseNameReply>`
:raises:
- :class:`InvalidBusNameError <dbus_next.InvalidBusNameError>` - If \
the given bus name is not valid.
- :class:`DBusError <dbus_next.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 <dbus_next.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 <dbus_next.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 <dbus_next.Message>`
:returns: A future that resolves when the message is sent or a
connection error occurs.
:rtype: :class:`Future <asyncio.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)

View File

@ -0,0 +1,183 @@
import xml.etree.ElementTree as ET
from typing import List, Union
from .. import introspection as intr
from .._private.util import replace_fds_with_idx, replace_idx_with_fds
from ..constants import ErrorType
from ..errors import DBusError
from ..message import Message, MessageFlag
from ..message_bus import BaseMessageBus
from ..proxy_object import BaseProxyInterface, BaseProxyObject
from ..signature import Variant
class ProxyInterface(BaseProxyInterface):
"""A class representing a proxy to an interface exported on the bus by
another client for the asyncio :class:`MessageBus
<dbus_next.aio.MessageBus>` implementation.
This class is not meant to be constructed directly by the user. Use
:func:`ProxyObject.get_interface()
<dbus_next.aio.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
<dbus_next.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 <dbus_next.glib.MessageBus>`.
For more information, see the :class:`BaseProxyObject <dbus_next.proxy_object.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()

108
src/dbus_fast/auth.py Normal file
View File

@ -0,0 +1,108 @@
import enum
import os
from .errors import AuthError
# 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 <dbus_next.message_bus.BaseMessageBus>` 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 <dbus_next.message_bus.BaseMessageBus>`.
: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 <dbus_next.message_bus.BaseMessageBus>`.
: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"

123
src/dbus_fast/constants.py Normal file
View File

@ -0,0 +1,123 @@
from 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"
)

79
src/dbus_fast/errors.py Normal file
View File

@ -0,0 +1,79 @@
class 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 .constants import ErrorType, MessageType
from .message import Message
from .validators import assert_interface_name_valid
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)

View File

@ -0,0 +1,2 @@
from .message_bus import MessageBus
from .proxy_object import ProxyInterface, ProxyObject

View File

@ -0,0 +1,496 @@
import io
from typing import Callable, Optional
from .. import introspection as intr
from .._private.unmarshaller import Unmarshaller
from ..auth import Authenticator, AuthExternal
from ..constants import (
BusType,
MessageFlag,
MessageType,
NameFlag,
ReleaseNameReply,
RequestNameReply,
)
from ..errors import AuthError
from ..message import Message
from ..message_bus import BaseMessageBus
from .proxy_object import ProxyObject
# 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() <dbus_next.glib.MessageBus.connect>` or
:func:`connect_sync() <dbus_next.glib.MessageBus.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 <dbus_next.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 <dbus_next.auth.AuthExternal>`.
:type auth: :class:`Authenticator <dbus_next.auth.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 <dbus_next.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 <dbus_next.glib.MessageBus>`
:raises:
- :class:`AuthError <dbus_next.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 <dbus_next.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 <dbus_next.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 <dbus_next.Message>`
:raises:
- :class:`DBusError <dbus_next.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 <dbus_next.introspection.Node>`
:raises:
- :class:`InvalidObjectPathError <dbus_next.InvalidObjectPathError>` \
- If the given object path is not valid.
- :class:`InvalidBusNameError <dbus_next.InvalidBusNameError>` - If \
the given bus name is not valid.
- :class:`DBusError <dbus_next.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 <dbus_next.NameFlag>`
:returns: The reply to the name request.
:rtype: :class:`RequestNameReply <dbus_next.RequestNameReply>`
:raises:
- :class:`InvalidBusNameError <dbus_next.InvalidBusNameError>` - If \
the given bus name is not valid.
- :class:`DBusError <dbus_next.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 <dbus_next.ReleaseNameReply>`
:raises:
- :class:`InvalidBusNameError <dbus_next.InvalidBusNameError>` - If \
the given bus name is not valid.
- :class:`DBusError <dbus_next.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

View File

@ -0,0 +1,309 @@
import xml.etree.ElementTree as ET
from typing import List, Union
from .. import introspection as intr
from ..constants import ErrorType
from ..errors import DBusError
from ..message import Message
from ..message_bus import BaseMessageBus
from ..proxy_object import BaseProxyInterface, BaseProxyObject
from ..signature import Variant
# 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 <dbus_next.glib.MessageBus>`
implementation.
This class is not meant to be constructed directly by the user. Use
:func:`ProxyObject.get_interface()
<dbus_next.glib.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 <dbus_next.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 <dbus_next.DBusError>` with
information about the error returned from the bus.
If the service returns an error for a synchronous DBus call, a
:class:`DBusError <dbus_next.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 <dbus_next.aio.MessageBus>`.
For more information, see the :class:`BaseProxyObject <dbus_next.proxy_object.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()

View File

@ -0,0 +1,594 @@
import xml.etree.ElementTree as ET
from typing import List, Union
from .constants import ArgDirection, PropertyAccess
from .errors import InvalidIntrospectionError
from .signature import SignatureTree, SignatureType
from .validators import assert_interface_name_valid, assert_member_name_valid
# 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 <dbus_next.ArgDirection>`
:ivar type: The parsed signature type of this argument.
:vartype type: :class:`SignatureType <dbus_next.SignatureType>`
:ivar signature: The signature string of this argument.
:vartype signature: str
:raises:
- :class:`InvalidMemberNameError <dbus_next.InvalidMemberNameError>` - If the name of the arg is not valid.
- :class:`InvalidSignatureError <dbus_next.InvalidSignatureError>` - If the signature is not valid.
- :class:`InvalidIntrospectionError <dbus_next.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 <dbus_next.ArgDirection>`
:raises:
- :class:`InvalidIntrospectionError <dbus_next.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 <dbus_next.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 <dbus_next.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 <dbus_next.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 <dbus_next.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 <dbus_next.PropertyAccess>`
:ivar type: The parsed type of this property.
:vartype type: :class:`SignatureType <dbus_next.SignatureType>`
:raises:
- :class:`InvalidIntrospectionError <dbus_next.InvalidIntrospectionError>` - If the property is not a single complete type.
- :class `InvalidSignatureError <dbus_next.InvalidSignatureError>` - If the given signature is not valid.
- :class: `InvalidMemberNameError <dbus_next.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 <dbus_next.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 <dbus_next.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 <dbus_next.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
<dbus_next.proxy_object.BaseProxyInterface>`. A :class:`ServiceInterface
<dbus_next.service.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 <dbus_next.introspection.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 <dbus_next.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 <dbus_next.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 <dbus_next.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 = '<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN"\n"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">\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"),
],
),
],
),
],
)

282
src/dbus_fast/message.py Normal file
View File

@ -0,0 +1,282 @@
from typing import Any, List
from ._private.constants import LITTLE_ENDIAN, PROTOCOL_VERSION, HeaderField
from ._private.marshaller import Marshaller
from .constants import ErrorType, MessageFlag, MessageType
from .errors import InvalidMessageError
from .signature import SignatureTree, Variant
from .validators import (
assert_bus_name_valid,
assert_interface_name_valid,
assert_member_name_valid,
assert_object_path_valid,
)
class Message:
"""A class for sending and receiving messages through the
:class:`MessageBus <dbus_next.message_bus.BaseMessageBus>` 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

1183
src/dbus_fast/message_bus.py Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,295 @@
import asyncio
import inspect
import logging
import re
import xml.etree.ElementTree as ET
from typing import Coroutine, List, Type, Union
from . import introspection as intr
from . import message_bus
from ._private.util import replace_idx_with_fds
from .constants import ErrorType, MessageType
from .errors import DBusError, InterfaceNotFoundError
from .message import Message
from .validators import assert_bus_name_valid, assert_object_path_valid
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 <dbus_next.introspection.Interface>`
:ivar bus: The message bus this proxy interface is connected to.
:vartype bus: :class:`BaseMessageBus <dbus_next.message_bus.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()
<dbus_next.message_bus.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 <dbus_next.introspection.Node>`
:ivar bus: The message bus this proxy object is connected to.
:vartype bus: :class:`BaseMessageBus <dbus_next.message_bus.BaseMessageBus>`
:ivar ~.ProxyInterface: The proxy interface class this proxy object uses.
:vartype ~.ProxyInterface: Type[:class:`BaseProxyInterface <dbus_next.proxy_object.BaseProxyObject>`]
:ivar child_paths: A list of absolute object paths of the children of this object.
:vartype child_paths: list(str)
:raises:
- :class:`InvalidBusNameError <dbus_next.InvalidBusNameError>` - If the given bus name is not valid.
- :class:`InvalidObjectPathError <dbus_next.InvalidObjectPathError>` - If the given object path is not valid.
- :class:`InvalidIntrospectionError <dbus_next.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 <dbus_next.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

578
src/dbus_fast/service.py Normal file
View File

@ -0,0 +1,578 @@
import asyncio
import copy
import inspect
from functools import wraps
from typing import Any, Dict, List, no_type_check_decorator
from . import introspection as intr
from ._private.util import (
parse_annotation,
replace_fds_with_idx,
replace_idx_with_fds,
signature_contains_type,
)
from .constants import PropertyAccess
from .errors import SignalDisabledError
from .signature import SignatureBodyMismatchError, SignatureTree, Variant
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 <dbus_next.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
<dbus_next.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 <dbus_next.message_bus.BaseMessageBus.export>`
method of a :class:`MessageBus <dbus_next.message_bus.BaseMessageBus>`.
Use the :func:`@method <dbus_next.service.method>`, :func:`@dbus_property
<dbus_next.service.dbus_property>`, and :func:`@signal
<dbus_next.service.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)

454
src/dbus_fast/signature.py Normal file
View File

@ -0,0 +1,454 @@
from typing import Any, List, Union
from .errors import InvalidSignatureError, SignatureBodyMismatchError
from .validators import is_object_path_valid
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 "<dbus_next.signature.Variant ('{}', {})>".format(
self.type.signature, self.value
)

190
src/dbus_fast/validators.py Normal file
View File

@ -0,0 +1,190 @@
import re
from .errors import (
InvalidBusNameError,
InvalidInterfaceNameError,
InvalidMemberNameError,
InvalidObjectPathError,
)
_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_]*$")
_member_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 _member_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)

View File

@ -0,0 +1,134 @@
from test.util import check_gi_repository, skip_reason_no_gi
import dbus_next.introspection as intr
import pytest
from dbus_next import DBusError, aio, glib
from dbus_next.message import MessageFlag
from dbus_next.service import ServiceInterface, method
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()

View File

@ -0,0 +1,124 @@
from test.util import check_gi_repository, skip_reason_no_gi
import pytest
from dbus_next import DBusError, Message, aio, glib
from dbus_next.service import PropertyAccess, ServiceInterface, dbus_property
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()

View File

@ -0,0 +1,227 @@
import pytest
from dbus_next import Message
from dbus_next.aio import MessageBus
from dbus_next.constants import RequestNameReply
from dbus_next.introspection import Node
from dbus_next.service import ServiceInterface, signal
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()

View File

@ -0,0 +1,29 @@
<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN"
"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
<node name="/com/example/sample_object0">
<interface name="com.example.SampleInterface0">
<method name="Frobate">
<arg name="foo" type="i" direction="in"/>
<arg name="bar" type="s" direction="out"/>
<arg name="baz" type="a{us}" direction="out"/>
<annotation name="org.freedesktop.DBus.Deprecated" value="true"/>
</method>
<method name="Bazify">
<arg name="bar" type="(iiu)" direction="in"/>
<arg name="bar" type="v" direction="out"/>
</method>
<method name="Mogrify">
<arg name="bar" type="(iiav)" direction="in"/>
</method>
<signal name="Changed">
<arg name="new_value" type="b"/>
</signal>
<signal name="ChangedMulti">
<arg name="new_value1" type="b"/>
<arg name="new_value2" type="y"/>
</signal>
<property name="Bar" type="y" access="write"/>
</interface>
<node name="child_of_sample_object"/>
<node name="another_child_of_sample_object"/>
</node>

234
tests/data/messages.json Normal file
View File

@ -0,0 +1,234 @@
[
{
"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"
}
]

View File

View File

@ -0,0 +1,151 @@
from dbus_next import PropertyAccess
from dbus_next import introspection as intr
from dbus_next.service import ServiceInterface, dbus_property, method, signal
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

View File

@ -0,0 +1,118 @@
import pytest
from dbus_next import Message, MessageType
from dbus_next import introspection as intr
from dbus_next.aio import MessageBus
from dbus_next.service import ServiceInterface, method
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

View File

@ -0,0 +1,180 @@
import pytest
from dbus_next import (
DBusError,
ErrorType,
Message,
MessageFlag,
MessageType,
SignatureTree,
Variant,
)
from dbus_next.aio import MessageBus
from dbus_next.service import ServiceInterface, method
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

View File

@ -0,0 +1,296 @@
import asyncio
import pytest
from dbus_next import (
DBusError,
ErrorType,
Message,
MessageType,
PropertyAccess,
Variant,
)
from dbus_next.aio import MessageBus
from dbus_next.service import ServiceInterface, dbus_property, method
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"],
]

View File

@ -0,0 +1,250 @@
import asyncio
import pytest
from dbus_next import Message, MessageType
from dbus_next.aio import MessageBus
from dbus_next.constants import PropertyAccess
from dbus_next.service import (
ServiceInterface,
SignalDisabledError,
dbus_property,
signal,
)
from dbus_next.signature import Variant
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"]],
)

View File

@ -0,0 +1,237 @@
import pytest
from dbus_next import Message, MessageType
from dbus_next import introspection as intr
from dbus_next.aio import MessageBus
from dbus_next.constants import ErrorType
from dbus_next.service import PropertyAccess, ServiceInterface, dbus_property
from dbus_next.signature import Variant
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 == [{}]

View File

@ -0,0 +1,28 @@
from 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

146
tests/test_aio_low_level.py Normal file
View File

@ -0,0 +1,146 @@
import pytest
from dbus_next import Message, MessageFlag, MessageType
from dbus_next.aio import MessageBus
@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"]

64
tests/test_big_message.py Normal file
View File

@ -0,0 +1,64 @@
from test.util import check_gi_repository, skip_reason_no_gi
import pytest
from dbus_next import Message, MessageType, aio, glib
from dbus_next.service import ServiceInterface, method
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]

62
tests/test_disconnect.py Normal file
View File

@ -0,0 +1,62 @@
import functools
import os
import pytest
from dbus_next import Message
from dbus_next.aio import MessageBus
@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()

371
tests/test_fd_passing.py Normal file
View File

@ -0,0 +1,371 @@
"""This tests the ability to send and receive file descriptors in dbus messages"""
import os
import pytest
from dbus_next import Message, MessageType
from dbus_next.aio import MessageBus
from dbus_next.service import ServiceInterface, dbus_property, method, signal
from dbus_next.signature import SignatureTree, Variant
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

View File

@ -0,0 +1,158 @@
from test.util import check_gi_repository, skip_reason_no_gi
import pytest
from dbus_next import Message, MessageFlag, MessageType
from dbus_next.glib import MessageBus
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"]

123
tests/test_introspection.py Normal file
View File

@ -0,0 +1,123 @@
import os
from dbus_next import ArgDirection, PropertyAccess, SignatureType
from dbus_next import introspection as intr
example_data = open(f"{os.path.dirname(__file__)}/data/introspection.xml").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

122
tests/test_marshaller.py Normal file
View File

@ -0,0 +1,122 @@
import io
import json
import os
from dbus_next import Message, SignatureTree, Variant
from dbus_next._private.unmarshaller import Unmarshaller
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]

View File

@ -0,0 +1,82 @@
from test.util import check_gi_repository, skip_reason_no_gi
import pytest
from dbus_next import (
Message,
MessageType,
NameFlag,
ReleaseNameReply,
RequestNameReply,
aio,
glib,
)
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()

221
tests/test_signature.py Normal file
View File

@ -0,0 +1,221 @@
import pytest
from dbus_next import SignatureBodyMismatchError, SignatureTree, Variant
from dbus_next._private.util import signature_contains_type
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])

70
tests/test_tcp_address.py Normal file
View File

@ -0,0 +1,70 @@
import asyncio
import os
import pytest
from dbus_next import Message
from dbus_next._private.address import parse_address
from dbus_next.aio import MessageBus
@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()

90
tests/test_validators.py Normal file
View File

@ -0,0 +1,90 @@
from dbus_next import (
is_bus_name_valid,
is_interface_name_valid,
is_member_name_valid,
is_object_path_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", "foo-bar"]
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}"'

16
tests/util.py Normal file
View File

@ -0,0 +1,16 @@
_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