chore: initial port
This commit is contained in:
parent
169581f691
commit
495bfac17f
20
docs/source/Makefile
Normal file
20
docs/source/Makefile
Normal 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
0
docs/source/_static/.gitignore
vendored
Normal file
0
docs/source/_templates/.gitignore
vendored
Normal file
0
docs/source/_templates/.gitignore
vendored
Normal file
9
docs/source/authentication.rst
Normal file
9
docs/source/authentication.rst
Normal 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
|
||||
@ -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 = []
|
||||
# This pattern also affects html_static_path and html_extra_path .
|
||||
exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
|
||||
|
||||
# The name of the Pygments (syntax highlighting) style to use.
|
||||
pygments_style = "sphinx"
|
||||
|
||||
# -- Options for HTML output -------------------------------------------------
|
||||
|
||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||
# a list of builtin themes.
|
||||
#
|
||||
html_theme = "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
39
docs/source/constants.rst
Normal 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
19
docs/source/errors.rst
Normal 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
|
||||
7
docs/source/high-level-client/aio-proxy-interface.rst
Normal file
7
docs/source/high-level-client/aio-proxy-interface.rst
Normal file
@ -0,0 +1,7 @@
|
||||
aio.ProxyInterface
|
||||
==================
|
||||
|
||||
.. autoclass:: dbus_next.aio.ProxyInterface
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
7
docs/source/high-level-client/aio-proxy-object.rst
Normal file
7
docs/source/high-level-client/aio-proxy-object.rst
Normal file
@ -0,0 +1,7 @@
|
||||
aio.ProxyObject
|
||||
===============
|
||||
|
||||
.. autoclass:: dbus_next.aio.ProxyObject
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
6
docs/source/high-level-client/base-proxy-interface.rst
Normal file
6
docs/source/high-level-client/base-proxy-interface.rst
Normal file
@ -0,0 +1,6 @@
|
||||
BaseProxyInterface
|
||||
==================
|
||||
|
||||
.. autoclass:: dbus_next.proxy_object.BaseProxyInterface
|
||||
:members:
|
||||
:undoc-members:
|
||||
6
docs/source/high-level-client/base-proxy-object.rst
Normal file
6
docs/source/high-level-client/base-proxy-object.rst
Normal file
@ -0,0 +1,6 @@
|
||||
BaseProxyObject
|
||||
===============
|
||||
|
||||
.. autoclass:: dbus_next.proxy_object.BaseProxyObject
|
||||
:members:
|
||||
:undoc-members:
|
||||
7
docs/source/high-level-client/glib-proxy-interface.rst
Normal file
7
docs/source/high-level-client/glib-proxy-interface.rst
Normal file
@ -0,0 +1,7 @@
|
||||
glib.ProxyInterface
|
||||
===================
|
||||
|
||||
.. autoclass:: dbus_next.glib.ProxyInterface
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
7
docs/source/high-level-client/glib-proxy-object.rst
Normal file
7
docs/source/high-level-client/glib-proxy-object.rst
Normal file
@ -0,0 +1,7 @@
|
||||
glib.ProxyObject
|
||||
================
|
||||
|
||||
.. autoclass:: dbus_next.glib.ProxyObject
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
100
docs/source/high-level-client/index.rst
Normal file
100
docs/source/high-level-client/index.rst
Normal 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()
|
||||
93
docs/source/high-level-service/index.rst
Normal file
93
docs/source/high-level-service/index.rst
Normal 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())
|
||||
12
docs/source/high-level-service/service-interface.rst
Normal file
12
docs/source/high-level-service/service-interface.rst
Normal 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
72
docs/source/index.rst
Normal 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`
|
||||
26
docs/source/introspection.rst
Normal file
26
docs/source/introspection.rst
Normal 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:
|
||||
100
docs/source/low-level-interface/index.rst
Normal file
100
docs/source/low-level-interface/index.rst
Normal 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)
|
||||
6
docs/source/low-level-interface/message.rst
Normal file
6
docs/source/low-level-interface/message.rst
Normal file
@ -0,0 +1,6 @@
|
||||
Message
|
||||
=======
|
||||
|
||||
.. autoclass:: dbus_next.Message
|
||||
:members:
|
||||
:undoc-members:
|
||||
7
docs/source/message-bus/aio-message-bus.rst
Normal file
7
docs/source/message-bus/aio-message-bus.rst
Normal file
@ -0,0 +1,7 @@
|
||||
aio.MessageBus
|
||||
==============
|
||||
|
||||
.. autoclass:: dbus_next.aio.MessageBus
|
||||
:members:
|
||||
:inherited-members:
|
||||
:show-inheritance:
|
||||
5
docs/source/message-bus/base-message-bus.rst
Normal file
5
docs/source/message-bus/base-message-bus.rst
Normal file
@ -0,0 +1,5 @@
|
||||
BaseMessageBus
|
||||
==============
|
||||
|
||||
.. autoclass:: dbus_next.message_bus.BaseMessageBus
|
||||
:members:
|
||||
7
docs/source/message-bus/glib-message-bus.rst
Normal file
7
docs/source/message-bus/glib-message-bus.rst
Normal file
@ -0,0 +1,7 @@
|
||||
glib.MessageBus
|
||||
===============
|
||||
|
||||
.. autoclass:: dbus_next.glib.MessageBus
|
||||
:members:
|
||||
:inherited-members:
|
||||
:show-inheritance:
|
||||
17
docs/source/message-bus/index.rst
Normal file
17
docs/source/message-bus/index.rst
Normal 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.
|
||||
102
docs/source/type-system/index.rst
Normal file
102
docs/source/type-system/index.rst
Normal 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>`__.
|
||||
6
docs/source/type-system/signature-tree.rst
Normal file
6
docs/source/type-system/signature-tree.rst
Normal file
@ -0,0 +1,6 @@
|
||||
SignatureTree
|
||||
=============
|
||||
|
||||
.. autoclass:: dbus_next.SignatureTree
|
||||
:members:
|
||||
:undoc-members:
|
||||
7
docs/source/type-system/signature-type.rst
Normal file
7
docs/source/type-system/signature-type.rst
Normal file
@ -0,0 +1,7 @@
|
||||
SignatureType
|
||||
==============
|
||||
|
||||
.. autoclass:: dbus_next.SignatureType
|
||||
:members:
|
||||
:undoc-members:
|
||||
:exclude-members: signature
|
||||
6
docs/source/type-system/variant.rst
Normal file
6
docs/source/type-system/variant.rst
Normal file
@ -0,0 +1,6 @@
|
||||
Variant
|
||||
=======
|
||||
|
||||
.. autoclass:: dbus_next.Variant
|
||||
:members:
|
||||
:undoc-members:
|
||||
11
docs/source/validators.rst
Normal file
11
docs/source/validators.rst
Normal 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
34
examples/aio-list-names.py
Executable 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())
|
||||
37
examples/aio-tcp-notification.py
Executable file
37
examples/aio-tcp-notification.py
Executable 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
138
examples/dbus-next-send.py
Executable 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
67
examples/example-service.py
Executable 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
38
examples/glib-list-names.py
Executable 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
48
examples/mpris.py
Executable 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())
|
||||
@ -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,
|
||||
)
|
||||
|
||||
8
src/dbus_fast/__version__.py
Normal file
8
src/dbus_fast/__version__.py
Normal 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"
|
||||
0
src/dbus_fast/_private/__init__.py
Normal file
0
src/dbus_fast/_private/__init__.py
Normal file
106
src/dbus_fast/_private/address.py
Normal file
106
src/dbus_fast/_private/address.py
Normal 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}")
|
||||
18
src/dbus_fast/_private/constants.py
Normal file
18
src/dbus_fast/_private/constants.py
Normal 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
|
||||
161
src/dbus_fast/_private/marshaller.py
Normal file
161
src/dbus_fast/_private/marshaller.py
Normal 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
|
||||
315
src/dbus_fast/_private/unmarshaller.py
Normal file
315
src/dbus_fast/_private/unmarshaller.py
Normal 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
|
||||
172
src/dbus_fast/_private/util.py
Normal file
172
src/dbus_fast/_private/util.py
Normal 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)
|
||||
2
src/dbus_fast/aio/__init__.py
Normal file
2
src/dbus_fast/aio/__init__.py
Normal file
@ -0,0 +1,2 @@
|
||||
from .message_bus import MessageBus
|
||||
from .proxy_object import ProxyInterface, ProxyObject
|
||||
463
src/dbus_fast/aio/message_bus.py
Normal file
463
src/dbus_fast/aio/message_bus.py
Normal 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)
|
||||
183
src/dbus_fast/aio/proxy_object.py
Normal file
183
src/dbus_fast/aio/proxy_object.py
Normal 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
108
src/dbus_fast/auth.py
Normal 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
123
src/dbus_fast/constants.py
Normal 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
79
src/dbus_fast/errors.py
Normal 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)
|
||||
2
src/dbus_fast/glib/__init__.py
Normal file
2
src/dbus_fast/glib/__init__.py
Normal file
@ -0,0 +1,2 @@
|
||||
from .message_bus import MessageBus
|
||||
from .proxy_object import ProxyInterface, ProxyObject
|
||||
496
src/dbus_fast/glib/message_bus.py
Normal file
496
src/dbus_fast/glib/message_bus.py
Normal 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
|
||||
309
src/dbus_fast/glib/proxy_object.py
Normal file
309
src/dbus_fast/glib/proxy_object.py
Normal 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()
|
||||
594
src/dbus_fast/introspection.py
Normal file
594
src/dbus_fast/introspection.py
Normal 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
282
src/dbus_fast/message.py
Normal 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
1183
src/dbus_fast/message_bus.py
Normal file
File diff suppressed because it is too large
Load Diff
295
src/dbus_fast/proxy_object.py
Normal file
295
src/dbus_fast/proxy_object.py
Normal 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
578
src/dbus_fast/service.py
Normal 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
454
src/dbus_fast/signature.py
Normal 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
190
src/dbus_fast/validators.py
Normal 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)
|
||||
134
tests/client/test_methods.py
Normal file
134
tests/client/test_methods.py
Normal 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()
|
||||
124
tests/client/test_properties.py
Normal file
124
tests/client/test_properties.py
Normal 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()
|
||||
227
tests/client/test_signals.py
Normal file
227
tests/client/test_signals.py
Normal 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()
|
||||
29
tests/data/introspection.xml
Normal file
29
tests/data/introspection.xml
Normal 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
234
tests/data/messages.json
Normal 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"
|
||||
}
|
||||
]
|
||||
0
tests/service/__init__.py
Normal file
0
tests/service/__init__.py
Normal file
151
tests/service/test_decorators.py
Normal file
151
tests/service/test_decorators.py
Normal 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
|
||||
118
tests/service/test_export.py
Normal file
118
tests/service/test_export.py
Normal 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
|
||||
180
tests/service/test_methods.py
Normal file
180
tests/service/test_methods.py
Normal 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
|
||||
296
tests/service/test_properties.py
Normal file
296
tests/service/test_properties.py
Normal 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"],
|
||||
]
|
||||
250
tests/service/test_signals.py
Normal file
250
tests/service/test_signals.py
Normal 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"]],
|
||||
)
|
||||
237
tests/service/test_standard_interfaces.py
Normal file
237
tests/service/test_standard_interfaces.py
Normal 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 == [{}]
|
||||
28
tests/test_address_parser.py
Normal file
28
tests/test_address_parser.py
Normal 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
146
tests/test_aio_low_level.py
Normal 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
64
tests/test_big_message.py
Normal 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
62
tests/test_disconnect.py
Normal 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
371
tests/test_fd_passing.py
Normal 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
|
||||
158
tests/test_glib_low_level.py
Normal file
158
tests/test_glib_low_level.py
Normal 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
123
tests/test_introspection.py
Normal 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
122
tests/test_marshaller.py
Normal 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]
|
||||
82
tests/test_request_name.py
Normal file
82
tests/test_request_name.py
Normal 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
221
tests/test_signature.py
Normal 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
70
tests/test_tcp_address.py
Normal 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
90
tests/test_validators.py
Normal 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
16
tests/util.py
Normal 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
|
||||
Loading…
x
Reference in New Issue
Block a user