├── .codecov.yml ├── .gitignore ├── .packit.yaml ├── .pylintrc ├── .readthedocs.yml ├── .travis.yml ├── .travis ├── Dockerfile.centos ├── Dockerfile.debian ├── Dockerfile.fedora ├── Dockerfile.python ├── Dockerfile.ubuntu └── requirements.txt ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.md ├── docs ├── Makefile ├── conf.py ├── development.rst ├── environment.yml ├── examples.rst ├── index.rst ├── make.bat └── pydbus.rst ├── examples ├── 01_hostname │ └── client.py ├── 02_notification │ ├── client.py │ ├── common.py │ └── listener.py ├── 03_helloworld │ ├── client.py │ ├── common.py │ └── server.py ├── 04_register │ ├── client.py │ ├── common.py │ ├── listener.py │ └── server.py ├── 05_chat │ ├── client.py │ ├── common.py │ ├── listener.py │ └── server.py └── 06_inhibit │ └── client.py ├── pyproject.toml ├── python-dasbus.spec ├── src └── dasbus │ ├── __init__.py │ ├── client │ ├── __init__.py │ ├── handler.py │ ├── observer.py │ ├── property.py │ └── proxy.py │ ├── connection.py │ ├── constants.py │ ├── error.py │ ├── identifier.py │ ├── loop.py │ ├── namespace.py │ ├── server │ ├── __init__.py │ ├── container.py │ ├── handler.py │ ├── interface.py │ ├── property.py │ ├── publishable.py │ └── template.py │ ├── signal.py │ ├── specification.py │ ├── structure.py │ ├── typing.py │ ├── unix.py │ └── xml.py └── tests ├── __init__.py ├── lib_dbus.py ├── test_client.py ├── test_connection.py ├── test_container.py ├── test_dbus.py ├── test_error.py ├── test_identifier.py ├── test_interface.py ├── test_namespace.py ├── test_observer.py ├── test_property.py ├── test_proxy.py ├── test_server.py ├── test_signal.py ├── test_specification.py ├── test_structure.py ├── test_typing.py ├── test_unix.py └── test_xml.py /.codecov.yml: -------------------------------------------------------------------------------- 1 | codecov: 2 | # Don't wait for all other statuses to pass. 3 | require_ci_to_pass: false 4 | 5 | coverage: 6 | status: 7 | project: 8 | # Show a strict status for the code. 9 | default: 10 | paths: 11 | - "src/" 12 | patch: 13 | # Show an informational status for the patch. 14 | default: 15 | informational: true 16 | 17 | # Disable the pull request comment. 18 | comment: false 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | docs/api/ 69 | 70 | # PyBuilder 71 | target/ 72 | 73 | # Jupyter Notebook 74 | .ipynb_checkpoints 75 | 76 | # pyenv 77 | .python-version 78 | 79 | # celery beat schedule file 80 | celerybeat-schedule 81 | 82 | # SageMath parsed files 83 | *.sage.py 84 | 85 | # Environments 86 | .env 87 | .venv 88 | env/ 89 | venv/ 90 | ENV/ 91 | env.bak/ 92 | venv.bak/ 93 | 94 | # Spyder project settings 95 | .spyderproject 96 | .spyproject 97 | 98 | # Rope project settings 99 | .ropeproject 100 | 101 | # mkdocs documentation 102 | /site 103 | 104 | # mypy 105 | .mypy_cache/ 106 | -------------------------------------------------------------------------------- /.packit.yaml: -------------------------------------------------------------------------------- 1 | # See the documentation for more information: 2 | # https://packit.dev/docs/configuration/ 3 | 4 | upstream_package_name: dasbus 5 | downstream_package_name: python-dasbus 6 | specfile_path: python-dasbus.spec 7 | upstream_tag_template: "v{version}" 8 | 9 | srpm_build_deps: 10 | - make 11 | - python3 12 | - python3-build 13 | 14 | actions: 15 | create-archive: 16 | - 'make archive' 17 | - 'bash -c "cp dist/*.tar.gz ."' 18 | - 'bash -c "ls *.tar.gz"' 19 | 20 | jobs: 21 | - job: tests 22 | trigger: pull_request 23 | targets: 24 | - fedora-rawhide 25 | 26 | - job: copr_build 27 | trigger: pull_request 28 | targets: 29 | - fedora-rawhide 30 | 31 | - job: copr_build 32 | trigger: commit 33 | targets: 34 | - fedora-rawhide 35 | branch: master 36 | owner: "@rhinstaller" 37 | project: Anaconda 38 | preserve_project: True 39 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | # Read the Docs configuration file 2 | # See: https://docs.readthedocs.io/en/stable/config-file/v2.html 3 | 4 | # The version of the configuration file. 5 | version: 2 6 | 7 | # Formats of the documentation to be built. 8 | formats: all 9 | 10 | # Configuration for Conda support. 11 | conda: 12 | environment: docs/environment.yml 13 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: minimal 2 | dist: focal 3 | services: docker 4 | sudo: required 5 | 6 | env: 7 | - IMAGE=python TAG=3.11-rc 8 | - IMAGE=python TAG=3.10 9 | - IMAGE=python TAG=3.9 10 | - IMAGE=python TAG=3.8 11 | - IMAGE=python TAG=3.7 12 | - IMAGE=fedora TAG=rawhide 13 | - IMAGE=centos TAG=stream9 14 | - IMAGE=centos TAG=stream8 15 | - IMAGE=debian TAG=latest 16 | - IMAGE=ubuntu TAG=devel 17 | 18 | before_install: 19 | # Create a docker image called "myimage". 20 | - docker build -t myimage -f ".travis/Dockerfile.${IMAGE}" --build-arg TAG="${TAG}" ".travis" 21 | 22 | install: 23 | # Install CI dependencies. 24 | pip install --user codecov 25 | 26 | before_script: 27 | # Create a docker container from the image, mount the current working 28 | # directory to /app inside the container and let the container running 29 | # in the background. The resulting container ID will be saved to 30 | # container_id so we can reference it later on. 31 | - docker run --volume "$(pwd):/app" --workdir "/app" --tty --detach myimage bash > container_id 32 | 33 | script: 34 | # Run tests in the running container. 35 | - docker exec "$(cat container_id)" make ci 36 | - docker exec "$(cat container_id)" make install test-install 37 | 38 | after_success: 39 | # Analyze the code coverage. 40 | - docker exec "$(cat container_id)" coverage3 xml -i 41 | - codecov 42 | 43 | after_script: 44 | # Stop the container. 45 | - docker stop "$(cat container_id)" 46 | -------------------------------------------------------------------------------- /.travis/Dockerfile.centos: -------------------------------------------------------------------------------- 1 | ARG TAG=stream9 2 | FROM quay.io/centos/centos:${TAG} 3 | ENV LC_CTYPE=C.UTF-8 4 | 5 | RUN cat /etc/os-release 6 | 7 | RUN dnf -y update && \ 8 | dnf -y install \ 9 | make \ 10 | python3 \ 11 | python3-pip \ 12 | python3-gobject-base \ 13 | dbus-daemon \ 14 | && dnf clean all 15 | 16 | COPY requirements.txt . 17 | 18 | RUN pip3 install -U pip && \ 19 | pip3 install -U -r requirements.txt 20 | -------------------------------------------------------------------------------- /.travis/Dockerfile.debian: -------------------------------------------------------------------------------- 1 | ARG TAG=latest 2 | FROM debian:${TAG} 3 | ENV LC_CTYPE=C.UTF-8 4 | 5 | RUN cat /etc/os-release 6 | 7 | RUN apt-get update && \ 8 | apt-get install -y \ 9 | make \ 10 | python3 \ 11 | python3-pip \ 12 | python3-venv \ 13 | python3-gi \ 14 | dbus-daemon \ 15 | && rm -rf /var/lib/apt/lists/* 16 | 17 | # Don't use the externally managed environment. 18 | RUN python3 -m venv --system-site-packages /opt/venv 19 | ENV VIRTUAL_ENV="/opt/venv" 20 | ENV PATH="$VIRTUAL_ENV/bin:$PATH" 21 | 22 | COPY requirements.txt . 23 | 24 | RUN pip3 install -U pip && \ 25 | pip3 install -U -r requirements.txt 26 | -------------------------------------------------------------------------------- /.travis/Dockerfile.fedora: -------------------------------------------------------------------------------- 1 | ARG TAG=rawhide 2 | FROM registry.fedoraproject.org/fedora:${TAG} 3 | ENV LC_CTYPE=C.UTF-8 4 | 5 | RUN cat /etc/os-release 6 | 7 | RUN dnf -y update && \ 8 | dnf -y install \ 9 | gcc \ 10 | make \ 11 | python3 \ 12 | python3-pip \ 13 | python3-gobject-base \ 14 | dbus-daemon \ 15 | && dnf clean all 16 | 17 | COPY requirements.txt . 18 | 19 | RUN pip3 install -U pip && \ 20 | pip3 install -U -r requirements.txt 21 | -------------------------------------------------------------------------------- /.travis/Dockerfile.python: -------------------------------------------------------------------------------- 1 | ARG TAG=latest 2 | FROM python:${TAG} 3 | ENV LC_CTYPE=C.UTF-8 4 | 5 | RUN cat /etc/os-release 6 | 7 | RUN apt-get update && \ 8 | apt-get install -y \ 9 | make \ 10 | gcc \ 11 | pkg-config \ 12 | python3-dev \ 13 | libgirepository1.0-dev \ 14 | dbus-daemon \ 15 | && rm -rf /var/lib/apt/lists/* 16 | 17 | COPY requirements.txt . 18 | 19 | RUN pip3 install -U pip && \ 20 | pip3 install -U PyGObject && \ 21 | pip3 install -U -r requirements.txt 22 | -------------------------------------------------------------------------------- /.travis/Dockerfile.ubuntu: -------------------------------------------------------------------------------- 1 | ARG TAG=devel 2 | FROM ubuntu:${TAG} 3 | ENV LC_CTYPE=C.UTF-8 4 | 5 | # Disable interaction with tzdata. 6 | ENV DEBIAN_FRONTEND=noninteractive 7 | 8 | RUN cat /etc/os-release 9 | 10 | RUN apt-get update && \ 11 | apt-get install -y \ 12 | make \ 13 | python3 \ 14 | python3-pip \ 15 | python3-venv \ 16 | python3-gi \ 17 | dbus-daemon \ 18 | && rm -rf /var/lib/apt/lists/* 19 | 20 | # Don't use the externally managed environment. 21 | RUN python3 -m venv --system-site-packages /opt/venv 22 | ENV VIRTUAL_ENV="/opt/venv" 23 | ENV PATH="$VIRTUAL_ENV/bin:$PATH" 24 | 25 | COPY requirements.txt . 26 | 27 | RUN pip3 install -U pip && \ 28 | pip3 install -U -r requirements.txt 29 | -------------------------------------------------------------------------------- /.travis/requirements.txt: -------------------------------------------------------------------------------- 1 | # Package list of build and test pip requirements 2 | 3 | # Documentation requirements 4 | sphinx 5 | sphinx_rtd_theme 6 | sphinxcontrib-apidoc 7 | 8 | # Testing requirements 9 | coverage 10 | pylint 11 | pytest 12 | 13 | # Build requirements 14 | build 15 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | # Include the README 2 | include *.md 3 | 4 | # Include the license file 5 | include LICENSE 6 | 7 | # Include the tests 8 | recursive-include tests *.py 9 | 10 | # Include the examples 11 | recursive-include examples *.py 12 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2019 Red Hat, Inc. All rights reserved. 2 | # 3 | # This library is free software; you can redistribute it and/or 4 | # modify it under the terms of the GNU Lesser General Public 5 | # License as published by the Free Software Foundation; either 6 | # version 2.1 of the License, or (at your option) any later version. 7 | # 8 | # This library is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 11 | # Lesser General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU Lesser General Public 14 | # License along with this library; if not, write to the Free Software 15 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 16 | # USA 17 | # 18 | PKGNAME = dasbus 19 | VERSION = $(shell awk '/Version:/ { print $$2 }' python-$(PKGNAME).spec) 20 | TAG = v$(VERSION) 21 | PYTHON ?= python3 22 | COVERAGE ?= coverage3 23 | 24 | # Container-related 25 | CI_NAME = $(PKGNAME)-ci 26 | CI_IMAGE ?= fedora 27 | CI_TAG ?= latest 28 | CI_CMD ?= make ci 29 | 30 | # Arguments used by pylint for checking the code. 31 | CHECK_ARGS ?= 32 | 33 | .PHONY: clean 34 | clean: 35 | git clean -idx 36 | 37 | .PHONY: container-ci 38 | container-ci: 39 | podman build \ 40 | --file ".travis/Dockerfile.$(CI_IMAGE)" \ 41 | --build-arg TAG=$(CI_TAG) \ 42 | --tag $(CI_NAME) \ 43 | --pull-always 44 | 45 | podman run \ 46 | --volume .:/dasbus:Z \ 47 | --workdir /dasbus \ 48 | $(CI_NAME) $(CI_CMD) 49 | 50 | .PHONY: ci 51 | ci: 52 | @echo "*** Running CI with $(PYTHON) ***" 53 | $(PYTHON) --version 54 | $(MAKE) check 55 | $(MAKE) test 56 | $(MAKE) docs 57 | 58 | .PHONY: check 59 | check: 60 | @echo "*** Running pylint ***" 61 | $(PYTHON) -m pylint --version 62 | $(PYTHON) -m pylint $(CHECK_ARGS) src/ tests/ 63 | 64 | .PHONY: test 65 | test: 66 | @echo "*** Running pytest with $(COVERAGE) ***" 67 | PYTHONPATH=src $(COVERAGE) run -m pytest 68 | $(COVERAGE) combine 69 | $(COVERAGE) report -m --include="src/*" | tee coverage-report.log 70 | 71 | .PHONY: test-install 72 | test-install: 73 | @echo "*** Running tests for the installed package ***" 74 | $(PYTHON) -c "import dasbus" 75 | $(PYTHON) -m pytest 76 | 77 | .PHONY: docs 78 | docs: 79 | $(MAKE) -C docs html text 80 | 81 | .PHONY: changelog 82 | changelog: 83 | @git log --no-merges --pretty="format:- %s (%ae)" $(TAG).. | sed -e 's/@.*)/)/' 84 | 85 | .PHONY: commit 86 | commit: 87 | @NEWSUBVER=$$((`echo $(VERSION) | cut -d . -f 2` + 1)) ; \ 88 | NEWVERSION=`echo $(VERSION).$$NEWSUBVER | cut -d . -f 1,3` ; \ 89 | DATELINE="* `LC_ALL=C.UTF-8 date "+%a %b %d %Y"` `git config user.name` <`git config user.email`> - $$NEWVERSION-1" ; \ 90 | cl=`grep -n %changelog python-${PKGNAME}.spec | cut -d : -f 1` ; \ 91 | tail --lines=+$$(($$cl + 1)) python-${PKGNAME}.spec > speclog ; \ 92 | (head -n $$cl python-${PKGNAME}.spec ; echo "$$DATELINE" ; make --quiet changelog 2>/dev/null ; echo ""; cat speclog) > python-${PKGNAME}.spec.new ; \ 93 | mv python-${PKGNAME}.spec.new python-${PKGNAME}.spec ; rm -f speclog ; \ 94 | sed -i "s/Version:\( *\)$(VERSION)/Version:\1$$NEWVERSION/" python-${PKGNAME}.spec ; \ 95 | sed -i "s/version = \"$(VERSION)\"/version = \"$$NEWVERSION\"/" pyproject.toml ; \ 96 | git add python-${PKGNAME}.spec setup.py ; \ 97 | git commit -m "New release: $$NEWVERSION" 98 | 99 | .PHONY: tag 100 | tag: 101 | git tag -a -m "Tag as $(VERSION)" -f $(TAG) 102 | @echo "Tagged as $(TAG)" 103 | 104 | .PHONY: push 105 | push: 106 | @echo "Run the command 'git push --follow-tags' with '--dry-run' first." 107 | 108 | .PHONY: archive 109 | archive: 110 | @echo "*** Building the distribution archive ***" 111 | $(PYTHON) -m build 112 | @echo "The archive is in dist/$(PKGNAME)-$(VERSION).tar.gz" 113 | 114 | .PHONY: install 115 | install: archive 116 | @echo "*** Installing the $(PKGNAME)-$(VERSION) package ***" 117 | $(PYTHON) -m pip install dist/$(PKGNAME)-$(VERSION)-py3-none-any.whl 118 | 119 | .PHONY: upload 120 | upload: 121 | $(PYTHON) -m twine upload dist/* 122 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # dasbus 2 | This DBus library is written in Python 3, based on GLib and inspired by pydbus. Find out more in 3 | the [documentation](https://dasbus.readthedocs.io/en/latest/). 4 | 5 | The code used to be part of the [Anaconda Installer](https://github.com/rhinstaller/anaconda) 6 | project. It was based on the [pydbus](https://github.com/LEW21/pydbus) library, but we replaced 7 | it with our own solution because its upstream development stalled. The dasbus library is 8 | a result of this effort. 9 | 10 | [![Build Status](https://travis-ci.com/rhinstaller/dasbus.svg?branch=master)](https://travis-ci.com/rhinstaller/dasbus) 11 | [![Documentation Status](https://readthedocs.org/projects/dasbus/badge/?version=latest)](https://dasbus.readthedocs.io/en/latest/?badge=latest) 12 | [![codecov](https://codecov.io/gh/rhinstaller/dasbus/branch/master/graph/badge.svg)](https://codecov.io/gh/rhinstaller/dasbus) 13 | 14 | ## Requirements 15 | 16 | * Python 3.6+ 17 | * PyGObject 3 18 | 19 | You can install [PyGObject](https://pygobject.readthedocs.io) provided by your operating system 20 | or use PyPI. The system package is usually called `python3-gi`, `python3-gobject` or `pygobject3`. 21 | See the [instructions](https://pygobject.readthedocs.io/en/latest/getting_started.html) for 22 | your platform (only for PyGObject, you don't need cairo or GTK). 23 | 24 | The library is known to work with Python 3.8, PyGObject 3.34 and GLib 2.63, but these are not the 25 | required minimal versions. 26 | 27 | ## Installation 28 | 29 | Install the package from [PyPI](https://pypi.org/project/dasbus/) or install the package 30 | provided by your operating system if available. 31 | 32 | ### Install from PyPI 33 | 34 | Follow the instructions above to install the requirements before you install `dasbus` with `pip`. 35 | The required dependencies has to be installed manually in this case. 36 | 37 | ``` 38 | pip3 install dasbus 39 | ``` 40 | 41 | ### Install the system package 42 | 43 | Follow the instructions for your operating system to install the `python-dasbus` package. 44 | The required dependencies should be installed automatically by the system package manager. 45 | 46 | * [Arch Linux](https://dasbus.readthedocs.io/en/latest/#install-on-arch-linux) 47 | * [Debian / Ubuntu](https://dasbus.readthedocs.io/en/latest/#install-on-debian-ubuntu) 48 | * [Fedora / CentOS / RHEL](https://dasbus.readthedocs.io/en/latest/#install-on-fedora-centos-rhel) 49 | * [openSUSE](https://dasbus.readthedocs.io/en/latest/#install-on-opensuse) 50 | 51 | ## Examples 52 | 53 | Show the current hostname. 54 | 55 | ```python 56 | from dasbus.connection import SystemMessageBus 57 | bus = SystemMessageBus() 58 | 59 | proxy = bus.get_proxy( 60 | "org.freedesktop.hostname1", 61 | "/org/freedesktop/hostname1" 62 | ) 63 | 64 | print(proxy.Hostname) 65 | ``` 66 | 67 | Send a notification to the notification server. 68 | 69 | ```python 70 | from dasbus.connection import SessionMessageBus 71 | bus = SessionMessageBus() 72 | 73 | proxy = bus.get_proxy( 74 | "org.freedesktop.Notifications", 75 | "/org/freedesktop/Notifications" 76 | ) 77 | 78 | id = proxy.Notify( 79 | "", 0, "face-smile", "Hello World!", 80 | "This notification can be ignored.", 81 | [], {}, 0 82 | ) 83 | 84 | print("The notification {} was sent.".format(id)) 85 | ``` 86 | 87 | Handle a closed notification. 88 | 89 | ```python 90 | from dasbus.loop import EventLoop 91 | loop = EventLoop() 92 | 93 | from dasbus.connection import SessionMessageBus 94 | bus = SessionMessageBus() 95 | 96 | proxy = bus.get_proxy( 97 | "org.freedesktop.Notifications", 98 | "/org/freedesktop/Notifications" 99 | ) 100 | 101 | def callback(id, reason): 102 | print("The notification {} was closed.".format(id)) 103 | 104 | proxy.NotificationClosed.connect(callback) 105 | loop.run() 106 | ``` 107 | 108 | Asynchronously fetch a list of network devices. 109 | 110 | ```python 111 | from dasbus.loop import EventLoop 112 | loop = EventLoop() 113 | 114 | from dasbus.connection import SystemMessageBus 115 | bus = SystemMessageBus() 116 | 117 | proxy = bus.get_proxy( 118 | "org.freedesktop.NetworkManager", 119 | "/org/freedesktop/NetworkManager" 120 | ) 121 | 122 | def callback(call): 123 | print(call()) 124 | 125 | proxy.GetDevices(callback=callback) 126 | loop.run() 127 | ``` 128 | 129 | Inhibit the system suspend and hibernation. 130 | 131 | ```python 132 | import os 133 | from dasbus.connection import SystemMessageBus 134 | from dasbus.unix import GLibClientUnix 135 | bus = SystemMessageBus() 136 | 137 | proxy = bus.get_proxy( 138 | "org.freedesktop.login1", 139 | "/org/freedesktop/login1", 140 | client=GLibClientUnix 141 | ) 142 | 143 | fd = proxy.Inhibit( 144 | "sleep", "my-example", "Running an example", "block" 145 | ) 146 | 147 | proxy.ListInhibitors() 148 | os.close(fd) 149 | ``` 150 | 151 | Define the org.example.HelloWorld service. 152 | 153 | ```python 154 | class HelloWorld(object): 155 | __dbus_xml__ = """ 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | """ 165 | 166 | def Hello(self, name): 167 | return "Hello {}!".format(name) 168 | ``` 169 | 170 | Define the org.example.HelloWorld service with an automatically generated XML specification. 171 | 172 | ```python 173 | from dasbus.server.interface import dbus_interface 174 | from dasbus.typing import Str 175 | 176 | @dbus_interface("org.example.HelloWorld") 177 | class HelloWorld(object): 178 | 179 | def Hello(self, name: Str) -> Str: 180 | return "Hello {}!".format(name) 181 | 182 | print(HelloWorld.__dbus_xml__) 183 | ``` 184 | 185 | Publish the org.example.HelloWorld service on the session message bus. 186 | 187 | ```python 188 | from dasbus.connection import SessionMessageBus 189 | bus = SessionMessageBus() 190 | bus.publish_object("/org/example/HelloWorld", HelloWorld()) 191 | bus.register_service("org.example.HelloWorld") 192 | 193 | from dasbus.loop import EventLoop 194 | loop = EventLoop() 195 | loop.run() 196 | ``` 197 | 198 | See more examples in the [documentation](https://dasbus.readthedocs.io/en/latest/examples.html). 199 | 200 | ## Inspiration 201 | 202 | Look at the [complete examples](https://github.com/rhinstaller/dasbus/tree/master/examples) or 203 | [DBus services](https://github.com/rhinstaller/anaconda/tree/master/pyanaconda/modules) of 204 | the Anaconda Installer for more inspiration. 205 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= -W --keep-going 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Configuration file for the Sphinx documentation builder. 3 | # 4 | # This file only contains a selection of the most common options. For a full 5 | # list see the documentation: 6 | # http://www.sphinx-doc.org/en/master/config 7 | 8 | # -- Path setup -------------------------------------------------------------- 9 | 10 | # If extensions (or modules to document with autodoc) are in another directory, 11 | # add these directories to sys.path here. If the directory is relative to the 12 | # documentation root, use os.path.abspath to make it absolute, like shown here. 13 | # 14 | import os 15 | import sys 16 | import sphinx_rtd_theme 17 | 18 | sys.path.insert(0, os.path.abspath('../src')) 19 | 20 | # -- Project information ----------------------------------------------------- 21 | 22 | project = 'dasbus' 23 | copyright = '2020, Vendula Poncova' 24 | author = 'Vendula Poncova' 25 | 26 | 27 | # -- General configuration --------------------------------------------------- 28 | 29 | # Add any Sphinx extension module names here, as strings. They can be 30 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 31 | # ones. 32 | extensions = [ 33 | 'sphinx.ext.autodoc', 34 | 'sphinx.ext.autosummary', 35 | 'sphinx.ext.doctest', 36 | 'sphinx.ext.intersphinx', 37 | 'sphinx.ext.todo', 38 | 'sphinx.ext.coverage', 39 | 'sphinx.ext.viewcode', 40 | 'sphinxcontrib.apidoc', 41 | 'sphinx_rtd_theme', 42 | ] 43 | 44 | # The path to a Python module to automatically document. 45 | apidoc_module_dir = '../src/dasbus' 46 | 47 | # Put documentation for each module on its own page. 48 | apidoc_separate_modules = True 49 | 50 | # Don't generate the api/modules.rst file. 51 | apidoc_toc_file = False 52 | 53 | # Add any paths that contain templates here, relative to this directory. 54 | templates_path = ['_templates'] 55 | 56 | # List of patterns, relative to source directory, that match files and 57 | # directories to ignore when looking for source files. 58 | # This pattern also affects html_static_path and html_extra_path. 59 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 60 | 61 | # A boolean that decides whether module names are prepended to all object names. 62 | add_module_names = False 63 | 64 | # -- Options for HTML output ------------------------------------------------- 65 | 66 | # The theme to use for HTML and HTML Help pages. See the documentation for 67 | # a list of builtin themes. 68 | # 69 | html_theme = 'sphinx_rtd_theme' 70 | 71 | # Add any paths that contain custom static files (such as style sheets) here, 72 | # relative to this directory. They are copied after the builtin static files, 73 | # so a file named "default.css" will overwrite the builtin "default.css". 74 | # html_static_path = ['_static'] 75 | -------------------------------------------------------------------------------- /docs/development.rst: -------------------------------------------------------------------------------- 1 | Development and testing 2 | ======================= 3 | 4 | Install `podman `_ and use the following command to run all tests 5 | in a container. It doesn't require any additional dependencies: 6 | 7 | .. code-block:: shell 8 | 9 | make container-ci 10 | 11 | Use the command below to run only the unit tests: 12 | 13 | .. code-block:: shell 14 | 15 | make container-ci CI_CMD="make test" 16 | -------------------------------------------------------------------------------- /docs/environment.yml: -------------------------------------------------------------------------------- 1 | # Conda environment file 2 | name: dasbus-docs-environment 3 | 4 | channels: 5 | - defaults 6 | - conda-forge 7 | 8 | dependencies: 9 | - python=3 10 | - pygobject 11 | - docutils<0.17 12 | - sphinxcontrib-apidoc 13 | -------------------------------------------------------------------------------- /docs/examples.rst: -------------------------------------------------------------------------------- 1 | Examples 2 | ======== 3 | 4 | Look at the `complete examples `_ or 5 | `DBus services `_ of 6 | the Anaconda Installer for more inspiration. 7 | 8 | Basic usage 9 | ----------- 10 | 11 | Show the current hostname. 12 | 13 | .. code-block:: python 14 | 15 | from dasbus.connection import SystemMessageBus 16 | bus = SystemMessageBus() 17 | 18 | proxy = bus.get_proxy( 19 | "org.freedesktop.hostname1", 20 | "/org/freedesktop/hostname1" 21 | ) 22 | 23 | print(proxy.Hostname) 24 | 25 | Send a notification to the notification server. 26 | 27 | .. code-block:: python 28 | 29 | from dasbus.connection import SessionMessageBus 30 | bus = SessionMessageBus() 31 | 32 | proxy = bus.get_proxy( 33 | "org.freedesktop.Notifications", 34 | "/org/freedesktop/Notifications" 35 | ) 36 | 37 | id = proxy.Notify( 38 | "", 0, "face-smile", "Hello World!", 39 | "This notification can be ignored.", 40 | [], {}, 0 41 | ) 42 | 43 | print("The notification {} was sent.".format(id)) 44 | 45 | Handle a closed notification. 46 | 47 | .. code-block:: python 48 | 49 | from dasbus.loop import EventLoop 50 | loop = EventLoop() 51 | 52 | from dasbus.connection import SessionMessageBus 53 | bus = SessionMessageBus() 54 | 55 | proxy = bus.get_proxy( 56 | "org.freedesktop.Notifications", 57 | "/org/freedesktop/Notifications" 58 | ) 59 | 60 | def callback(id, reason): 61 | print("The notification {} was closed.".format(id)) 62 | 63 | proxy.NotificationClosed.connect(callback) 64 | loop.run() 65 | 66 | Asynchronously fetch a list of network devices. 67 | 68 | .. code-block:: python 69 | 70 | from dasbus.loop import EventLoop 71 | loop = EventLoop() 72 | 73 | from dasbus.connection import SystemMessageBus 74 | bus = SystemMessageBus() 75 | 76 | proxy = bus.get_proxy( 77 | "org.freedesktop.NetworkManager", 78 | "/org/freedesktop/NetworkManager" 79 | ) 80 | 81 | def callback(call): 82 | print(call()) 83 | 84 | proxy.GetDevices(callback=callback) 85 | loop.run() 86 | 87 | Define the org.example.HelloWorld service. 88 | 89 | .. code-block:: python 90 | 91 | class HelloWorld(object): 92 | __dbus_xml__ = """ 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | """ 102 | 103 | def Hello(self, name): 104 | return "Hello {}!".format(name) 105 | 106 | Define the org.example.HelloWorld service with an automatically generated XML specification. 107 | 108 | .. code-block:: python 109 | 110 | from dasbus.server.interface import dbus_interface 111 | from dasbus.typing import Str 112 | 113 | @dbus_interface("org.example.HelloWorld") 114 | class HelloWorld(object): 115 | 116 | def Hello(self, name: Str) -> Str: 117 | return "Hello {}!".format(name) 118 | 119 | print(HelloWorld.__dbus_xml__) 120 | 121 | Publish the org.example.HelloWorld service on the session message bus. 122 | 123 | .. code-block:: python 124 | 125 | from dasbus.connection import SessionMessageBus 126 | bus = SessionMessageBus() 127 | bus.publish_object("/org/example/HelloWorld", HelloWorld()) 128 | bus.register_service("org.example.HelloWorld") 129 | 130 | from dasbus.loop import EventLoop 131 | loop = EventLoop() 132 | loop.run() 133 | 134 | Support for Unix file descriptors 135 | --------------------------------- 136 | 137 | The support for Unix file descriptors is disabled by default. It needs to be explicitly enabled 138 | when you create a DBus proxy or publish a DBus object that could send or receive Unix file 139 | descriptors. 140 | 141 | .. warning:: 142 | 143 | This functionality is supported only on UNIX. 144 | 145 | Send and receive Unix file descriptors with a DBus proxy. 146 | 147 | .. code-block:: python 148 | 149 | import os 150 | from dasbus.connection import SystemMessageBus 151 | from dasbus.unix import GLibClientUnix 152 | bus = SystemMessageBus() 153 | 154 | proxy = bus.get_proxy( 155 | "org.freedesktop.login1", 156 | "/org/freedesktop/login1", 157 | client=GLibClientUnix 158 | ) 159 | 160 | fd = proxy.Inhibit( 161 | "sleep", "my-example", "Running an example", "block" 162 | ) 163 | 164 | proxy.ListInhibitors() 165 | os.close(fd) 166 | 167 | Allow to send and receive Unix file descriptors within the /org/example/HelloWorld DBus object. 168 | 169 | .. code-block:: python 170 | 171 | from dasbus.unix import GLibServerUnix 172 | bus.publish_object( 173 | "/org/example/HelloWorld", 174 | HelloWorld(), 175 | server=GLibServerUnix 176 | ) 177 | 178 | Management of DBus names and paths 179 | ---------------------------------- 180 | 181 | Use constants to define DBus services and objects. 182 | 183 | .. code-block:: python 184 | 185 | from dasbus.connection import SystemMessageBus 186 | from dasbus.identifier import DBusServiceIdentifier, DBusObjectIdentifier 187 | 188 | NETWORK_MANAGER_NAMESPACE = ( 189 | "org", "freedesktop", "NetworkManager" 190 | ) 191 | 192 | NETWORK_MANAGER = DBusServiceIdentifier( 193 | namespace=NETWORK_MANAGER_NAMESPACE, 194 | message_bus=SystemMessageBus() 195 | ) 196 | 197 | NETWORK_MANAGER_SETTINGS = DBusObjectIdentifier( 198 | namespace=NETWORK_MANAGER_NAMESPACE, 199 | basename="Settings" 200 | ) 201 | 202 | Create a proxy of the org.freedesktop.NetworkManager service. 203 | 204 | .. code-block:: python 205 | 206 | proxy = NETWORK_MANAGER.get_proxy() 207 | print(proxy.NetworkingEnabled) 208 | 209 | Create a proxy of the /org/freedesktop/NetworkManager/Settings object. 210 | 211 | .. code-block:: python 212 | 213 | proxy = NETWORK_MANAGER.get_proxy(NETWORK_MANAGER_SETTINGS) 214 | print(proxy.Hostname) 215 | 216 | See `a complete example `__. 217 | 218 | Error handling 219 | -------------- 220 | 221 | Use exceptions to propagate and handle DBus errors. Create an error mapper and a decorator for 222 | mapping Python exception classes to DBus error names. 223 | 224 | .. code-block:: python 225 | 226 | from dasbus.error import ErrorMapper, DBusError, get_error_decorator 227 | error_mapper = ErrorMapper() 228 | dbus_error = get_error_decorator(error_mapper) 229 | 230 | Use the decorator to register Python exceptions that represent DBus errors. These exceptions 231 | can be raised by DBus services and caught by DBus clients in the try-except block. 232 | 233 | .. code-block:: python 234 | 235 | @dbus_error("org.freedesktop.DBus.Error.InvalidArgs") 236 | class InvalidArgs(DBusError): 237 | pass 238 | 239 | The message bus will use the specified error mapper to automatically transform Python exceptions 240 | to DBus errors and back. 241 | 242 | .. code-block:: python 243 | 244 | from dasbus.connection import SessionMessageBus 245 | bus = SessionMessageBus(error_mapper=error_mapper) 246 | 247 | See `a complete example `__. 248 | 249 | Timeout for a DBus call 250 | ----------------------- 251 | 252 | Call DBus methods with a timeout (specified in milliseconds). 253 | 254 | .. code-block:: python 255 | 256 | proxy = NETWORK_MANAGER.get_proxy() 257 | 258 | try: 259 | proxy.CheckConnectivity(timeout=3) 260 | except TimeoutError: 261 | print("The call timed out!") 262 | 263 | 264 | Support for DBus structures 265 | --------------------------- 266 | 267 | Represent DBus structures by Python objects. A DBus structure is a dictionary of attributes that 268 | maps attribute names to variants with attribute values. Use Python objects to define such 269 | structures. They can be easily converted to a dictionary, send via DBus and converted back to 270 | an object. 271 | 272 | .. code-block:: python 273 | 274 | from dasbus.structure import DBusData 275 | from dasbus.typing import Str, get_variant 276 | 277 | class UserData(DBusData): 278 | def __init__(self): 279 | self._name = "" 280 | 281 | @property 282 | def name(self) -> Str: 283 | return self._name 284 | 285 | @name.setter 286 | def name(self, name): 287 | self._name = name 288 | 289 | data = UserData() 290 | data.name = "Alice" 291 | 292 | print(UserData.to_structure(data)) 293 | print(UserData.from_structure({ 294 | "name": get_variant(Str, "Bob") 295 | })) 296 | 297 | See `a complete example `__. 298 | 299 | Management of dynamic DBus objects 300 | ---------------------------------- 301 | 302 | Create Python objects that can be automatically published on DBus. These objects are usually 303 | managed by DBus containers and published on demand. 304 | 305 | .. code-block:: python 306 | 307 | from dasbus.server.interface import dbus_interface 308 | from dasbus.server.template import InterfaceTemplate 309 | from dasbus.server.publishable import Publishable 310 | from dasbus.typing import Str 311 | 312 | @dbus_interface("org.example.Chat") 313 | class ChatInterface(InterfaceTemplate): 314 | 315 | def Send(self, message: Str): 316 | return self.implementation.send() 317 | 318 | class Chat(Publishable): 319 | 320 | def for_publication(self): 321 | return ChatInterface(self) 322 | 323 | def send(self, message): 324 | print(message) 325 | 326 | Use DBus containers to automatically publish dynamically created Python objects. A DBus container 327 | converts publishable Python objects into DBus paths and back. It generates unique DBus paths in 328 | the specified namespace and assigns them to objects. Each object is published when its DBus path 329 | is requested for the first time. 330 | 331 | .. code-block:: python 332 | 333 | from dasbus.connection import SessionMessageBus 334 | from dasbus.server.container import DBusContainer 335 | 336 | container = DBusContainer( 337 | namespace=("org", "example", "Chat"), 338 | message_bus=SessionMessageBus() 339 | ) 340 | 341 | print(container.to_object_path(Chat())) 342 | 343 | See `a complete example `__. 344 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Welcome to dasbus's documentation! 2 | ================================== 3 | 4 | Dasbus is a DBus library written in Python 3, based on GLib and inspired by pydbus. 5 | The code used to be part of the `Anaconda Installer `_ 6 | project. It was based on the `pydbus `_ library, but we replaced 7 | it with our own solution because its upstream development stalled. The dasbus library is a result 8 | of this effort. 9 | 10 | Requirements 11 | ------------ 12 | 13 | - Python 3.6+ 14 | - PyGObject 3 15 | 16 | You can install `PyGObject `_ provided by your operating system 17 | or use PyPI. The system package is usually called ``python3-gi``, ``python3-gobject`` or 18 | ``pygobject3``. See the `instructions `_ 19 | for your platform (only for PyGObject, you don't need cairo or GTK). 20 | 21 | The library is known to work with Python 3.8, PyGObject 3.34 and GLib 2.63, but these are not the 22 | required minimal versions. 23 | 24 | 25 | Installation 26 | ------------ 27 | 28 | Install the package from `PyPI `_ or install the package 29 | provided by your operating system if available. 30 | 31 | Install from PyPI 32 | ^^^^^^^^^^^^^^^^^ 33 | 34 | Follow the instructions above to install the requirements before you install ``dasbus`` with 35 | ``pip``. The required dependencies has to be installed manually in this case. 36 | 37 | :: 38 | 39 | pip3 install dasbus 40 | 41 | Install on Arch Linux 42 | ^^^^^^^^^^^^^^^^^^^^^ 43 | 44 | Build and install the community package from the `Arch User Repository `_. 45 | Follow the `guidelines `_. 46 | 47 | :: 48 | 49 | git clone https://aur.archlinux.org/python-dasbus.git 50 | cd python-dasbus 51 | makepkg -si 52 | 53 | Install on Debian / Ubuntu 54 | ^^^^^^^^^^^^^^^^^^^^^^^^^^ 55 | 56 | Install the system package on Debian 11+ or Ubuntu 22.04+. 57 | 58 | :: 59 | 60 | sudo apt install python3-dasbus 61 | 62 | Install on Fedora / CentOS / RHEL 63 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 64 | 65 | Install the system package on Fedora 31+, CentOS Stream 8+ or RHEL 8+. 66 | 67 | :: 68 | 69 | sudo dnf install python3-dasbus 70 | 71 | Install on openSUSE 72 | ^^^^^^^^^^^^^^^^^^^ 73 | 74 | Install the system package on openSUSE Tumbleweed or openSUSE Leap 15.2+. 75 | 76 | :: 77 | 78 | sudo zypper install python3-dasbus 79 | 80 | 81 | .. toctree:: 82 | :maxdepth: 1 83 | :caption: Contents: 84 | 85 | Development and testing 86 | Dasbus vs pydbus 87 | API reference 88 | Examples 89 | 90 | Indices and tables 91 | ------------------ 92 | 93 | - :ref:`genindex` 94 | - :ref:`modindex` 95 | - :ref:`search` 96 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/pydbus.rst: -------------------------------------------------------------------------------- 1 | dasbus vs pydbus 2 | ================ 3 | 4 | The dasbus library used to be based on `pydbus `_, but it was 5 | later reimplemented. We have changed the API and the implementation of the library based on our 6 | experience with pydbus. However, it should be possible to modify dasbus classes to work the same 7 | way as pydbus classes. 8 | 9 | What is new 10 | ----------- 11 | 12 | - Support for asynchronous DBus calls: DBus methods can be called asynchronously. 13 | 14 | - Support for Unix file descriptors: It is possible to send or receive Unix file descriptors. 15 | 16 | - Mapping DBus errors to exceptions: Use Python exceptions to propagate and handle DBus errors. 17 | Define your own rules for mapping errors to exceptions and back. See the 18 | :class:`ErrorMapper ` class 19 | 20 | - Support for type hints: Use Python type hints from :mod:`dasbus.typing` to define DBus types. 21 | 22 | - Generating XML specifications: Automatically generate XML specifications from Python classes 23 | with the :func:`dbus_interface ` decorator. 24 | 25 | - Support for DBus structures: Represent DBus structures (dictionaries of variants) by Python 26 | objects. See the :class:`DBusData ` class. 27 | 28 | - Support for groups of DBus objects: Use DBus containers from :mod:`dasbus.server.container` 29 | to publish groups of Python objects. 30 | 31 | - Composition over inheritance: The library follows the principle of composition over 32 | inheritance. It allows to easily change the default behaviour. 33 | 34 | - Lazy DBus connections: DBus connections are established on demand. 35 | 36 | - Lazy DBus proxies: Attributes of DBus proxies are created on demand. 37 | 38 | 39 | What is different 40 | ----------------- 41 | 42 | - No context managers: There are no context managers in dasbus. Context managers and event 43 | loops don't work very well together. 44 | 45 | - No auto-completion: There is no support for automatic completion of DBus names and paths. 46 | We recommend to work with constants defined by classes from :mod:`dasbus.identifier` 47 | instead of strings. 48 | 49 | - No unpacking of variants: The dasbus library doesn't unpack variants by default. It means 50 | that values received from DBus match the types declared in the XML specification. Use the 51 | :func:`get_native ` function to unpack the values. 52 | 53 | - Obtaining proxy objects: Call the :meth:`get_proxy ` 54 | method to get a proxy of the specified DBus object. 55 | 56 | - No single-interface view: DBus proxies don't support single-interface views. Use the 57 | :class:`InterfaceProxy ` class to access a specific 58 | interface of a DBus object. 59 | 60 | - Higher priority of standard interfaces: If there is a DBus interface in the XML specification 61 | that redefines a member of a standard interface, the DBus proxy will choose a member of the 62 | standard interface. Use the :class:`InterfaceProxy ` class 63 | to access a specific interface of a DBus object. 64 | 65 | - No support for help: Members of DBus proxies are created lazily, so the build-in ``help`` 66 | function doesn't return useful information about the DBus interfaces. 67 | 68 | - Watching DBus names: Use :class:`a service observer ` 69 | to watch a DBus name. 70 | 71 | - Acquiring DBus names: Call the :meth:`register_service ` 72 | method to acquire a DBus name. 73 | 74 | - Providing XML specifications: Use the ``__dbus_xml__`` attribute to provide the XML 75 | specification of a DBus object. Or you can generate it from the code using the 76 | :func:`dbus_interface ` decorator. 77 | 78 | - No support for polkit: There is no support for the DBus service ``org.freedesktop.PolicyKit1``. 79 | 80 | What is the same (for now) 81 | -------------------------- 82 | 83 | - No support for other event loops: Dasbus uses GLib as its backend, so it requires to use 84 | the GLib event loop. However, the GLib part of dasbus is separated from the rest of the code, 85 | so it shouldn't be too difficult to add support for a different backend. It would be necessary 86 | to replace :class:`dasbus.typing.Variant` and :class:`dasbus.typing.VariantType` with their 87 | abstractions and reorganize the code. 88 | 89 | - No support for org.freedesktop.DBus.ObjectManager: There is no support for object managers, 90 | however the :class:`DBus containers ` could be a good 91 | starting point. 92 | -------------------------------------------------------------------------------- /examples/01_hostname/client.py: -------------------------------------------------------------------------------- 1 | # 2 | # Show the current hostname. 3 | # 4 | from dasbus.connection import SystemMessageBus 5 | 6 | if __name__ == "__main__": 7 | # Create a representation of a system bus connection. 8 | bus = SystemMessageBus() 9 | 10 | # Create a proxy of the object /org/freedesktop/hostname1 11 | # provided by the service org.freedesktop.hostname1. 12 | proxy = bus.get_proxy( 13 | "org.freedesktop.hostname1", 14 | "/org/freedesktop/hostname1" 15 | ) 16 | 17 | # Print a value of the DBus property Hostname. 18 | print(proxy.Hostname) 19 | -------------------------------------------------------------------------------- /examples/02_notification/client.py: -------------------------------------------------------------------------------- 1 | # 2 | # Send a notification to the notification server. 3 | # 4 | from common import NOTIFICATIONS 5 | 6 | if __name__ == "__main__": 7 | # Create a proxy of the object /org/freedesktop/Notifications 8 | # provided by the service org.freedesktop.Notifications. 9 | proxy = NOTIFICATIONS.get_proxy() 10 | 11 | # Call the DBus method Notify. 12 | notification_id = proxy.Notify( 13 | "", 0, "face-smile", "Hello World!", 14 | "This notification can be ignored.", 15 | [], {}, 0 16 | ) 17 | 18 | # Print the return value of the call. 19 | print("The notification {} was sent.".format(notification_id)) 20 | -------------------------------------------------------------------------------- /examples/02_notification/common.py: -------------------------------------------------------------------------------- 1 | # 2 | # The common definitions 3 | # 4 | from dasbus.connection import SessionMessageBus 5 | from dasbus.identifier import DBusServiceIdentifier 6 | 7 | # Define the message bus. 8 | SESSION_BUS = SessionMessageBus() 9 | 10 | # Define services and objects. 11 | NOTIFICATIONS = DBusServiceIdentifier( 12 | namespace=("org", "freedesktop", "Notifications"), 13 | message_bus=SESSION_BUS 14 | ) 15 | -------------------------------------------------------------------------------- /examples/02_notification/listener.py: -------------------------------------------------------------------------------- 1 | # 2 | # Handle a closed notification. 3 | # Start the listener, run the client and close a notification. 4 | # 5 | from dasbus.loop import EventLoop 6 | from common import NOTIFICATIONS 7 | 8 | 9 | def callback(notification_id, reason): 10 | """The callback of the DBus signal NotificationClosed.""" 11 | print("The notification {} was closed.".format(notification_id)) 12 | 13 | 14 | if __name__ == "__main__": 15 | # Create a proxy of the object /org/freedesktop/Notifications 16 | # provided by the service org.freedesktop.Notifications. 17 | proxy = NOTIFICATIONS.get_proxy() 18 | 19 | # Connect the callback to the DBus signal NotificationClosed. 20 | proxy.NotificationClosed.connect(callback) 21 | 22 | # Start the event loop. 23 | loop = EventLoop() 24 | loop.run() 25 | -------------------------------------------------------------------------------- /examples/03_helloworld/client.py: -------------------------------------------------------------------------------- 1 | # 2 | # Say hello to the world. 3 | # Start the server and run the client. 4 | # 5 | from common import HELLO_WORLD 6 | 7 | if __name__ == "__main__": 8 | # Create a proxy of the object /org/example/HelloWorld 9 | # provided by the service org.example.HelloWorld 10 | proxy = HELLO_WORLD.get_proxy() 11 | 12 | # Call the DBus method Hello and print the return value. 13 | print(proxy.Hello("World")) 14 | -------------------------------------------------------------------------------- /examples/03_helloworld/common.py: -------------------------------------------------------------------------------- 1 | # 2 | # The common definitions 3 | # 4 | from dasbus.connection import SessionMessageBus 5 | from dasbus.identifier import DBusServiceIdentifier 6 | 7 | # Define the message bus. 8 | SESSION_BUS = SessionMessageBus() 9 | 10 | # Define services and objects. 11 | HELLO_WORLD = DBusServiceIdentifier( 12 | namespace=("org", "example", "HelloWorld"), 13 | message_bus=SESSION_BUS 14 | ) 15 | -------------------------------------------------------------------------------- /examples/03_helloworld/server.py: -------------------------------------------------------------------------------- 1 | # 2 | # Run the service org.example.HelloWorld. 3 | # 4 | from dasbus.loop import EventLoop 5 | from dasbus.server.interface import dbus_interface 6 | from dasbus.typing import Str 7 | from common import HELLO_WORLD, SESSION_BUS 8 | from dasbus.xml import XMLGenerator 9 | 10 | 11 | @dbus_interface(HELLO_WORLD.interface_name) 12 | class HelloWorld(object): 13 | """The DBus interface for HelloWorld.""" 14 | 15 | def Hello(self, name: Str) -> Str: 16 | """Generate a greeting. 17 | 18 | :param name: someone to say hello 19 | :return: a greeting 20 | """ 21 | return "Hello {}!".format(name) 22 | 23 | 24 | if __name__ == "__main__": 25 | # Print the generated XML specification. 26 | print(XMLGenerator.prettify_xml(HelloWorld.__dbus_xml__)) 27 | 28 | try: 29 | # Create an instance of the class HelloWorld. 30 | hello_world = HelloWorld() 31 | 32 | # Publish the instance at /org/example/HelloWorld. 33 | SESSION_BUS.publish_object(HELLO_WORLD.object_path, hello_world) 34 | 35 | # Register the service name org.example.HelloWorld. 36 | SESSION_BUS.register_service(HELLO_WORLD.service_name) 37 | 38 | # Start the event loop. 39 | loop = EventLoop() 40 | loop.run() 41 | finally: 42 | # Unregister the DBus service and objects. 43 | SESSION_BUS.disconnect() 44 | -------------------------------------------------------------------------------- /examples/04_register/client.py: -------------------------------------------------------------------------------- 1 | # 2 | # Register the user Alice. 3 | # Start the server and run the client twice. 4 | # 5 | from common import REGISTER, User, InvalidUser 6 | 7 | if __name__ == "__main__": 8 | # Create a proxy of the object /org/example/Register 9 | # provided by the service org.example.Register 10 | proxy = REGISTER.get_proxy() 11 | 12 | # Register Alice. 13 | alice = User() 14 | alice.name = "Alice" 15 | alice.age = 1000 16 | 17 | print("Sending a DBus structure:") 18 | print(User.to_structure(alice)) 19 | 20 | try: 21 | proxy.RegisterUser(User.to_structure(alice)) 22 | except InvalidUser as e: 23 | print("Failed to register a user:", e) 24 | exit(1) 25 | 26 | # Print the registered users. 27 | print("Receiving DBus structures:") 28 | for user in proxy.Users: 29 | print(user) 30 | 31 | print("Registered users:") 32 | for user in User.from_structure_list(proxy.Users): 33 | print(user.name) 34 | -------------------------------------------------------------------------------- /examples/04_register/common.py: -------------------------------------------------------------------------------- 1 | # 2 | # The common definitions 3 | # 4 | from dasbus.connection import SessionMessageBus 5 | from dasbus.error import DBusError, ErrorMapper, get_error_decorator 6 | from dasbus.identifier import DBusServiceIdentifier 7 | from dasbus.structure import DBusData 8 | from dasbus.typing import Str, Int 9 | 10 | # Define the error mapper. 11 | ERROR_MAPPER = ErrorMapper() 12 | 13 | # Define the message bus. 14 | SESSION_BUS = SessionMessageBus( 15 | error_mapper=ERROR_MAPPER 16 | ) 17 | 18 | # Define namespaces. 19 | REGISTER_NAMESPACE = ("org", "example", "Register") 20 | 21 | # Define services and objects. 22 | REGISTER = DBusServiceIdentifier( 23 | namespace=REGISTER_NAMESPACE, 24 | message_bus=SESSION_BUS 25 | ) 26 | 27 | # The decorator for DBus errors. 28 | dbus_error = get_error_decorator(ERROR_MAPPER) 29 | 30 | 31 | # Define errors. 32 | @dbus_error("InvalidUserError", namespace=REGISTER_NAMESPACE) 33 | class InvalidUser(DBusError): 34 | """The user is invalid.""" 35 | pass 36 | 37 | 38 | # Define structures. 39 | class User(DBusData): 40 | """The user data.""" 41 | 42 | def __init__(self): 43 | self._name = "" 44 | self._age = 0 45 | 46 | @property 47 | def name(self) -> Str: 48 | """Name of the user.""" 49 | return self._name 50 | 51 | @name.setter 52 | def name(self, value: Str): 53 | self._name = value 54 | 55 | @property 56 | def age(self) -> Int: 57 | """Age of the user.""" 58 | return self._age 59 | 60 | @age.setter 61 | def age(self, value: Int): 62 | self._age = value 63 | -------------------------------------------------------------------------------- /examples/04_register/listener.py: -------------------------------------------------------------------------------- 1 | # 2 | # Handle changed properties. 3 | # Start the server, start the listener and run the client. 4 | # 5 | from dasbus.loop import EventLoop 6 | from common import REGISTER 7 | 8 | 9 | def callback(interface, changed_properties, invalid_properties): 10 | """The callback of the DBus signal PropertiesChanged.""" 11 | print("Properties of {} has changed: {}".format( 12 | interface, changed_properties 13 | )) 14 | 15 | 16 | if __name__ == "__main__": 17 | # Create a proxy of the object /org/example/Register 18 | # provided by the service org.example.Register 19 | proxy = REGISTER.get_proxy() 20 | 21 | # Connect the callback to the DBus signal PropertiesChanged. 22 | proxy.PropertiesChanged.connect(callback) 23 | 24 | # Start the event loop. 25 | loop = EventLoop() 26 | loop.run() 27 | -------------------------------------------------------------------------------- /examples/04_register/server.py: -------------------------------------------------------------------------------- 1 | # 2 | # Run the service org.example.Register. 3 | # 4 | from dasbus.loop import EventLoop 5 | from dasbus.server.interface import dbus_interface 6 | from dasbus.server.property import emits_properties_changed 7 | from dasbus.server.template import InterfaceTemplate 8 | from dasbus.signal import Signal 9 | from dasbus.typing import Structure, List 10 | from dasbus.xml import XMLGenerator 11 | from common import SESSION_BUS, REGISTER, User, InvalidUser 12 | 13 | 14 | @dbus_interface(REGISTER.interface_name) 15 | class RegisterInterface(InterfaceTemplate): 16 | """The DBus interface of the user register.""" 17 | 18 | def connect_signals(self): 19 | """Connect the signals.""" 20 | self.watch_property("Users", self.implementation.users_changed) 21 | 22 | @property 23 | def Users(self) -> List[Structure]: 24 | """The list of users.""" 25 | return User.to_structure_list(self.implementation.users) 26 | 27 | @emits_properties_changed 28 | def RegisterUser(self, user: Structure): 29 | """Register a new user.""" 30 | self.implementation.register_user(User.from_structure(user)) 31 | 32 | 33 | class Register(object): 34 | """The implementation of the user register.""" 35 | 36 | def __init__(self): 37 | self._users = [] 38 | self._users_changed = Signal() 39 | 40 | @property 41 | def users(self): 42 | """The list of users.""" 43 | return self._users 44 | 45 | @property 46 | def users_changed(self): 47 | """Signal the user list change.""" 48 | return self._users_changed 49 | 50 | def register_user(self, user: User): 51 | """Register a new user.""" 52 | if any(u for u in self.users if u.name == user.name): 53 | raise InvalidUser("User {} exists.".format(user.name)) 54 | 55 | self._users.append(user) 56 | self._users_changed.emit() 57 | 58 | 59 | if __name__ == "__main__": 60 | # Print the generated XML specification. 61 | print(XMLGenerator.prettify_xml(RegisterInterface.__dbus_xml__)) 62 | 63 | try: 64 | # Create the register. 65 | register = Register() 66 | 67 | # Publish the register at /org/example/Register. 68 | SESSION_BUS.publish_object( 69 | REGISTER.object_path, 70 | RegisterInterface(register) 71 | ) 72 | 73 | # Register the service name org.example.Register. 74 | SESSION_BUS.register_service( 75 | REGISTER.service_name 76 | ) 77 | 78 | # Start the event loop. 79 | loop = EventLoop() 80 | loop.run() 81 | finally: 82 | # Unregister the DBus service and objects. 83 | SESSION_BUS.disconnect() 84 | -------------------------------------------------------------------------------- /examples/05_chat/client.py: -------------------------------------------------------------------------------- 1 | # 2 | # Send a message to the chat room. 3 | # 4 | from common import CHAT 5 | 6 | if __name__ == "__main__": 7 | # Create a proxy of the object /org/example/Chat 8 | # provided by the service org.example.Chat 9 | chat_proxy = CHAT.get_proxy() 10 | 11 | # Get an object path of the chat room. 12 | object_path = chat_proxy.FindRoom("Bob's room") 13 | print("Bob's room:", object_path) 14 | 15 | # Create a proxy of the object /org/example/Chat/Rooms/1 16 | # provided by the service org.example.Chat 17 | room_proxy = CHAT.get_proxy(object_path) 18 | 19 | # Send a message to the chat room. 20 | room_proxy.SendMessage("Hi, I am Alice!") 21 | 22 | # Get an object path of the chat room. 23 | object_path = chat_proxy.FindRoom("Alice's room") 24 | print("Alice's room:", object_path) 25 | 26 | # Create a proxy of the object /org/example/Chat/Rooms/2 27 | # provided by the service org.example.Chat 28 | room_proxy = CHAT.get_proxy(object_path) 29 | 30 | # Send a message to the chat room. 31 | room_proxy.SendMessage("I am Alice!") 32 | -------------------------------------------------------------------------------- /examples/05_chat/common.py: -------------------------------------------------------------------------------- 1 | # 2 | # The common definitions 3 | # 4 | from dasbus.connection import SessionMessageBus 5 | from dasbus.identifier import DBusServiceIdentifier, DBusInterfaceIdentifier 6 | from dasbus.server.container import DBusContainer 7 | 8 | # Define the message bus. 9 | SESSION_BUS = SessionMessageBus() 10 | 11 | # Define namespaces. 12 | CHAT_NAMESPACE = ("org", "example", "Chat") 13 | ROOMS_NAMESPACE = (*CHAT_NAMESPACE, "Rooms") 14 | 15 | # Define services and objects. 16 | CHAT = DBusServiceIdentifier( 17 | namespace=CHAT_NAMESPACE, 18 | message_bus=SESSION_BUS 19 | ) 20 | 21 | ROOM = DBusInterfaceIdentifier( 22 | namespace=CHAT_NAMESPACE, 23 | basename="Room" 24 | ) 25 | 26 | # Define containers. 27 | ROOM_CONTAINER = DBusContainer( 28 | namespace=ROOMS_NAMESPACE, 29 | message_bus=SESSION_BUS 30 | ) 31 | -------------------------------------------------------------------------------- /examples/05_chat/listener.py: -------------------------------------------------------------------------------- 1 | # 2 | # Reply to a message in the chat room. 3 | # Start the server, start the listener and run the client. 4 | # 5 | from dasbus.loop import EventLoop 6 | from common import CHAT 7 | 8 | 9 | def callback(proxy, msg): 10 | """The callback of the DBus signal MessageReceived.""" 11 | if "I am Alice!" in msg: 12 | proxy.SendMessage("Hello Alice, I am Bob.") 13 | 14 | 15 | if __name__ == "__main__": 16 | # Create a proxy of the object /org/example/Chat 17 | # provided by the service org.example.Chat. 18 | chat_proxy = CHAT.get_proxy() 19 | 20 | # Find a chat room to monitor. 21 | object_path = chat_proxy.FindRoom("Bob's room") 22 | 23 | # Create a proxy of the object /org/example/Chat/Rooms/1 24 | # provided by the service org.example.Chat. 25 | room_proxy = CHAT.get_proxy(object_path) 26 | 27 | # Connect the callback to the DBus signal MessageReceived. 28 | room_proxy.MessageReceived.connect(lambda msg: callback(room_proxy, msg)) 29 | 30 | # Start the event loop. 31 | loop = EventLoop() 32 | loop.run() 33 | -------------------------------------------------------------------------------- /examples/05_chat/server.py: -------------------------------------------------------------------------------- 1 | # 2 | # Run the service org.example.Chat. 3 | # 4 | from dasbus.loop import EventLoop 5 | from dasbus.server.interface import dbus_interface, dbus_signal 6 | from dasbus.server.publishable import Publishable 7 | from dasbus.server.template import InterfaceTemplate 8 | from dasbus.signal import Signal 9 | from dasbus.typing import Str, ObjPath 10 | from dasbus.xml import XMLGenerator 11 | from common import SESSION_BUS, CHAT, ROOM, ROOM_CONTAINER 12 | 13 | 14 | @dbus_interface(ROOM.interface_name) 15 | class RoomInterface(InterfaceTemplate): 16 | """The DBus interface of the chat room.""" 17 | 18 | def connect_signals(self): 19 | """Connect the signals.""" 20 | self.implementation.message_received.connect(self.MessageReceived) 21 | 22 | @dbus_signal 23 | def MessageReceived(self, msg: Str): 24 | """Signal that a message has been received.""" 25 | pass 26 | 27 | def SendMessage(self, msg: Str): 28 | """Send a message to the chat room.""" 29 | self.implementation.send_message(msg) 30 | 31 | 32 | class Room(Publishable): 33 | """The implementation of the chat room.""" 34 | 35 | def __init__(self, name): 36 | self._name = name 37 | self._message_received = Signal() 38 | 39 | def for_publication(self): 40 | """Return a DBus representation.""" 41 | return RoomInterface(self) 42 | 43 | @property 44 | def message_received(self): 45 | """Signal that a message has been received.""" 46 | return self._message_received 47 | 48 | def send_message(self, msg): 49 | """Send a message to the chat room.""" 50 | print("{}: {}".format(self._name, msg)) 51 | self.message_received.emit(msg) 52 | 53 | 54 | @dbus_interface(CHAT.interface_name) 55 | class ChatInterface(InterfaceTemplate): 56 | """The DBus interface of the chat service.""" 57 | 58 | def FindRoom(self, name: Str) -> ObjPath: 59 | """Find or create a chat room.""" 60 | return ROOM_CONTAINER.to_object_path( 61 | self.implementation.find_room(name) 62 | ) 63 | 64 | 65 | class Chat(Publishable): 66 | """The implementation of the chat.""" 67 | 68 | def __init__(self): 69 | self._rooms = {} 70 | 71 | def for_publication(self): 72 | """Return a DBus representation.""" 73 | return ChatInterface(self) 74 | 75 | def find_room(self, name): 76 | """Find or create a chat room.""" 77 | if name not in self._rooms: 78 | self._rooms[name] = Room(name) 79 | 80 | return self._rooms[name] 81 | 82 | 83 | if __name__ == "__main__": 84 | # Print the generated XML specifications. 85 | print(XMLGenerator.prettify_xml(ChatInterface.__dbus_xml__)) 86 | print(XMLGenerator.prettify_xml(RoomInterface.__dbus_xml__)) 87 | 88 | try: 89 | # Create the chat. 90 | chat = Chat() 91 | 92 | # Publish the chat at /org/example/Chat. 93 | SESSION_BUS.publish_object(CHAT.object_path, chat.for_publication()) 94 | 95 | # Register the service name org.example.Chat. 96 | SESSION_BUS.register_service(CHAT.service_name) 97 | 98 | # Start the event loop. 99 | loop = EventLoop() 100 | loop.run() 101 | finally: 102 | # Unregister the DBus service and objects. 103 | SESSION_BUS.disconnect() 104 | -------------------------------------------------------------------------------- /examples/06_inhibit/client.py: -------------------------------------------------------------------------------- 1 | # 2 | # Inhibit the system suspend and hibernation. 3 | # 4 | import os 5 | from dasbus.connection import SystemMessageBus 6 | from dasbus.unix import GLibClientUnix 7 | 8 | if __name__ == "__main__": 9 | # Create a representation of a system bus connection. 10 | bus = SystemMessageBus() 11 | 12 | # Create a proxy of the /org/freedesktop/login1 object 13 | # provided by the org.freedesktop.login1 service with 14 | # an enabled support for Unix file descriptors. 15 | proxy = bus.get_proxy( 16 | "org.freedesktop.login1", 17 | "/org/freedesktop/login1", 18 | client=GLibClientUnix 19 | ) 20 | 21 | # Inhibit sleep by this example. 22 | print("Inhibit sleep by my-example.") 23 | fd = proxy.Inhibit( 24 | "sleep", 25 | "my-example", 26 | "Running an example", 27 | "block" 28 | ) 29 | 30 | # List active inhibitors. 31 | print("Active inhibitors:") 32 | for inhibitor in sorted(proxy.ListInhibitors()): 33 | print("\t".join(map(str, inhibitor))) 34 | 35 | # Release the inhibition lock. 36 | print("Release the inhibition lock.") 37 | os.close(fd) 38 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | # Python package build system requirements and information. See: 2 | # https://pip.pypa.io/en/stable/reference/build-system/pyproject-toml/ 3 | 4 | # Build configuration 5 | [build-system] 6 | requires = ["hatchling"] 7 | build-backend = "hatchling.build" 8 | 9 | # Project configuration 10 | [project] 11 | name = "dasbus" 12 | version = "1.7" 13 | description = "DBus library in Python 3" 14 | requires-python = ">=3.6" 15 | classifiers = [ 16 | "Programming Language :: Python :: 3", 17 | "License :: OSI Approved :: GNU Lesser General Public License v2 or later (LGPLv2+)", 18 | "Operating System :: OS Independent", 19 | "Intended Audience :: Developers", 20 | "Development Status :: 5 - Production/Stable", 21 | ] 22 | keywords = [ 23 | "dbus", 24 | "glib", 25 | "library" 26 | ] 27 | authors = [ 28 | {name="Vendula Poncova", email="vponcova@redhat.com"}, 29 | ] 30 | readme = "README.md" 31 | 32 | [project.urls] 33 | "Homepage" = "https://github.com/rhinstaller/dasbus" 34 | "Bug Tracker" = "https://github.com/rhinstaller/dasbus/issues" 35 | "Documentation" = "https://dasbus.readthedocs.io" 36 | 37 | # Coverage configuration 38 | [tool.coverage.run] 39 | branch = true 40 | parallel = true 41 | source = [ 42 | "src", 43 | "tests", 44 | ] 45 | concurrency = [ 46 | "multiprocessing", 47 | "thread", 48 | ] 49 | 50 | [tool.coverage.report] 51 | exclude_lines = [ 52 | "pragma: no cover", 53 | "raise AssertionError", 54 | "raise NotImplementedError", 55 | "@abstractmethod", 56 | ] 57 | 58 | # Pytest configuration 59 | [tool.pytest.ini_options] 60 | testpaths = [ 61 | "tests" 62 | ] 63 | python_files = [ 64 | "tests/*.py" 65 | ] 66 | addopts = "-vv" 67 | log_level = "NOTSET" 68 | -------------------------------------------------------------------------------- /python-dasbus.spec: -------------------------------------------------------------------------------- 1 | %global srcname dasbus 2 | 3 | Name: python-%{srcname} 4 | Version: 1.7 5 | Release: 1%{?dist} 6 | Summary: DBus library in Python 3 7 | 8 | License: LGPL-2.1-or-later 9 | URL: https://pypi.python.org/pypi/dasbus 10 | %if %{defined suse_version} 11 | Source0: %{srcname}-%{version}.tar.gz 12 | Group: Development/Libraries/Python 13 | %else 14 | Source0: %{pypi_source} 15 | %endif 16 | 17 | BuildArch: noarch 18 | 19 | %global _description %{expand: 20 | Dasbus is a DBus library written in Python 3, based on 21 | GLib and inspired by pydbus. It is designed to be easy 22 | to use and extend.} 23 | 24 | %description %{_description} 25 | 26 | %package -n python3-%{srcname} 27 | Summary: %{summary} 28 | BuildRequires: python3-devel 29 | %if %{defined suse_version} 30 | BuildRequires: fdupes 31 | BuildRequires: python-rpm-macros 32 | Requires: python3-gobject 33 | %else 34 | Requires: python3-gobject-base 35 | %endif 36 | %{?python_provide:%python_provide python3-%{srcname}} 37 | 38 | %description -n python3-%{srcname} %{_description} 39 | 40 | %prep 41 | %autosetup -n %{srcname}-%{version} 42 | 43 | %generate_buildrequires 44 | %pyproject_buildrequires 45 | 46 | %build 47 | %pyproject_wheel 48 | 49 | %install 50 | %pyproject_install 51 | %if %{defined suse_version} 52 | %python_expand %fdupes %{buildroot}%{python3_sitelib} 53 | %endif 54 | 55 | %pyproject_save_files %{srcname} 56 | 57 | %files -n python3-%{srcname} -f %{pyproject_files} 58 | %doc README.md 59 | 60 | %changelog 61 | * Mon Nov 07 2022 Vendula Poncova - 1.7-1 62 | - CI: Use dnf instead of yum to install CentOS packages (vponcova) 63 | - Documentation: Improve the installation instruction (vponcova) 64 | - Remove untracked files from the git repository interactively (vponcova) 65 | - UnixFD: Document the support for Unix file descriptors (vponcova) 66 | - Documentation: Clean up examples in the documentation (vponcova) 67 | - Documentation: Simplify the README.md file (vponcova) 68 | - Documentation: Fix bullet point lists (vponcova) 69 | - Documentation: Simplify the hostname example (vponcova) 70 | - CI: Run tests for all supported Python version (vponcova) 71 | - UnixFD: Handle DBus signals with Unix file descriptors (vponcova) 72 | - UnixFD: Add tests for DBus properties with Unix file descriptors (vponcova) 73 | - UnixFD: Clean up tests of DBus calls with Unix file descriptors (vponcova) 74 | - UnixFD: Clean up tests for swapping Unix file descriptors (vponcova) 75 | - UnixFD: Clean up `GLibClientUnix` and `GLibServerUnix` (vponcova) 76 | - UnixFD: Process results of client calls in the low-level library (vponcova) 77 | - UnixFD: Move the support for Unix file descriptors to dasbus.unix (vponcova) 78 | - CI: Always pull the latest container image (vponcova) 79 | - CI: Disable the unhashable-member warning (vponcova) 80 | - Revert "Don't use pylint from pip on Fedora Rawhide" (vponcova) 81 | - UnixFD: Move the unit tests to a new file (vponcova) 82 | - UnixFD: Manage the testing bus on set up and tear down (vponcova) 83 | - UnixFD: Don't add arguments to the DBusTestCase.setUp method (vponcova) 84 | - UnixFD: Create a new testing DBus interface (vponcova) 85 | - UnixFD: Fix the indentation in unit tests (vponcova) 86 | - Add unit tests for variants with variant types (vponcova) 87 | - Simplify the code for replacing values of the UnixFD type (vponcova) 88 | - Add classes for unpacking and unwrapping a variant (vponcova) 89 | - Don't use pylint from pip on Fedora Rawhide (vponcova) 90 | - UnixFD: Rename a parameter to server_arguments (vponcova) 91 | - UnixFD: Revert a change in GLibClient._async_call_finish (vponcova) 92 | - Raise TimeoutError if a DBus call times out (vponcova) 93 | - Fix pylint tests in CentOS Stream 8 (vponcova) 94 | - Fix the ENV instruction in Dockerfiles (vponcova) 95 | - Fix pylint issues (vponcova) 96 | - run forked tests using subprocess, instead of multiprocessing (wdouglass) 97 | - use mutable list for return value in fd_test_async make fd getters more explicit (wdouglass) 98 | - Add test case for method call only returning fd (jlyda) 99 | - Always use call_with_unix_fd_list* to properly handle returned fds (jlyda) 100 | - fix some lint discovered errors (wdouglass) 101 | - seperate unixfd functionality, to better support systems that don't have them (wdouglass) 102 | - Remove note in documentation about unsupported Unix file descriptors (wdouglass) 103 | - Add a test for UnixFD transfer (wdouglass) 104 | - Allow UnixFDs to be replaced and passed into Gio (wdouglass) 105 | - Fix rpm lint warnings for OpenSUSE 15.3 (christopher.m.cantalupo) 106 | - Extend the .coveragerc file (vponcova) 107 | - Disable builds for Fedora ELN on commits (vponcova) 108 | - Test Debian with Travis (vponcova) 109 | - Test Ubuntu with Travis (vponcova) 110 | - Test CentOS Stream 9 with Travis (vponcova) 111 | - Use CentOS Stream 8 for testing (vponcova) 112 | - add remove dbus object function on bus and update tests (matthewcaswell) 113 | - properly measure coverage across multiprocess test cases (wdouglass) 114 | - Move handle typing tests into a new class (and a new file) (wdouglass) 115 | - Add another test for a crazy data type, fix a bug discovered via the test (wdouglass) 116 | - Add functions for generating/consuming fdlists with variants (wdouglass) 117 | - Provide a language argument for the code blocks (seahawk1986) 118 | - Change the type of 'h' glib objects from 'File' to 'UnixFD' (wdouglass) 119 | - Allow to run tests in a container (vponcova) 120 | - Add C0209 to the ignore list for pylint (tjoslin) 121 | - Use the latest distro in Travis CI (vponcova) 122 | - Always update the container (vponcova) 123 | - Document limitations of the DBus specification generator (vponcova) 124 | * Mon May 31 2021 Vendula Poncova - 1.6-1 125 | - Add support for SUSE packaging in spec file (christopher.m.cantalupo) 126 | - Allow to generate multiple output arguments (vponcova) 127 | - Support multiple output arguments (vponcova) 128 | - Add the is_tuple_of_one function (vponcova) 129 | - Configure the codecov tool (vponcova) 130 | * Mon May 03 2021 Vendula Poncova - 1.5-1 131 | - Disable builds for Fedora ELN on pull requests (vponcova) 132 | - Provide additional info about the DBus call (vponcova) 133 | - Run the codecov uploader from a package (vponcova) 134 | - Switch to packit new fedora-latest alias (jkonecny) 135 | - Add daily builds for our Fedora-devel COPR repository (jkonecny) 136 | - Use Fedora container registry instead of Dockerhub (jkonecny) 137 | - Migrate daily COPR builds to Packit (jkonecny) 138 | - Switch Packit tests to copr builds instead (jkonecny) 139 | - Enable Packit build in ELN chroot (jkonecny) 140 | - Rename TestMessageBus class to silence pytest warning (luca) 141 | - Fix the raise-missing-from warning (vponcova) 142 | * Fri Jul 24 2020 Vendula Poncova - 1.4-1 143 | - Handle all errors of the DBus call (vponcova) 144 | - Fix tests for handling DBus errors on the server side (vponcova) 145 | - Run packit smoke tests for all Fedora (jkonecny) 146 | - Fix packit archive creation (jkonecny) 147 | - Add possibility to change setup.py arguments (jkonecny) 148 | * Wed Jun 17 2020 Vendula Poncova - 1.3-1 149 | - Document differences between dasbus and pydbus (vponcova) 150 | - Improve the support for interface proxies in the service identifier (vponcova) 151 | - Improve the support for interface proxies in the message bus (vponcova) 152 | - Test the interface proxies (vponcova) 153 | - Make the message bus of a service identifier accessible (vponcova) 154 | - Fix the testing environment for Fedora Rawhide (vponcova) 155 | * Mon May 18 2020 Vendula Poncova - 1.2-1 156 | - Replace ABC with ABCMeta (vponcova) 157 | - Fix typing tests (vponcova) 158 | - Run tests on the latest CentOS (vponcova) 159 | - Install sphinx from PyPI (vponcova) 160 | * Thu May 14 2020 Vendula Poncova - 1.1-1 161 | - Include tests and examples in the source distribution (vponcova) 162 | - Fix the pylint warning signature-differs (vponcova) 163 | * Tue May 05 2020 Vendula Poncova - 1.0-1 164 | - Fix the documentation (vponcova) 165 | - Fix minor typos (yurchor) 166 | - Enable Codecov (vponcova) 167 | - Test the documentation build (vponcova) 168 | - Extend the documentation (vponcova) 169 | - Add configuration files for Read the Docs and Conda (vponcova) 170 | - Fix all warnings from the generated documentation (vponcova) 171 | * Wed Apr 08 2020 Vendula Poncova - 0.4-1 172 | - Replace the error register with the error mapper (vponcova) 173 | - Propagate additional arguments for the client handler factory (vponcova) 174 | - Propagate additional arguments in the class AddressedMessageBus (vponcova) 175 | - Generate the documentation (vponcova) 176 | * Thu Apr 02 2020 Vendula Poncova - 0.3-1 177 | - Remove generate_dictionary_from_data (vponcova) 178 | - Improve some of the error messages (vponcova) 179 | - Check the list of DBus structures to convert (vponcova) 180 | - Add the Inspiration section to README (vponcova) 181 | - Enable syntax highlighting in README (vponcova) 182 | - Use the class EventLoop in README (vponcova) 183 | - Use the --no-merges option (vponcova) 184 | - Clean up the Makefile (vponcova) 185 | - Add examples (vponcova) 186 | - Add the representation of the event loop (vponcova) 187 | - Enable copr builds and add packit config (dhodovsk) 188 | - Extend README (vponcova) 189 | * Mon Jan 13 2020 Vendula Poncova - 0.2-1 190 | - Unwrap DBus values (vponcova) 191 | - Unwrap a variant data type (vponcova) 192 | - Add a default DBus error (vponcova) 193 | - Use the minimal image in Travis CI (vponcova) 194 | - Remove GLibErrorHandler (vponcova) 195 | - Remove map_error and map_by_default (vponcova) 196 | - Extend arguments of dbus_error (vponcova) 197 | - Extend arguments of dbus_interface (vponcova) 198 | - The list of callbacks in signals can be changed during emitting (vponcova) 199 | - Don't import from mock (vponcova) 200 | - Enable checks in Travis CI (vponcova) 201 | - Fix too long lines (vponcova) 202 | - Don't use wildcard imports (vponcova) 203 | - Add the check target to the Makefile (vponcova) 204 | - Enable Travis CI (vponcova) 205 | - Catch logged warnings in the unit tests (vponcova) 206 | - Add the coverage target to the Makefile (vponcova) 207 | - Rename tests (vponcova) 208 | - Create Makefile (vponcova) 209 | - Create a .spec file (vponcova) 210 | - Add requirements to the README file (vponcova) 211 | 212 | * Thu Oct 31 2019 Vendula Poncova - 0.1-1 213 | - Initial package 214 | -------------------------------------------------------------------------------- /src/dasbus/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dasbus-project/dasbus/be51b94b083bad6fa0716ad6dc97d12f4462f8d4/src/dasbus/__init__.py -------------------------------------------------------------------------------- /src/dasbus/client/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dasbus-project/dasbus/be51b94b083bad6fa0716ad6dc97d12f4462f8d4/src/dasbus/client/__init__.py -------------------------------------------------------------------------------- /src/dasbus/client/observer.py: -------------------------------------------------------------------------------- 1 | # 2 | # Client support for DBus observers 3 | # 4 | # Copyright (C) 2019 Red Hat, Inc. All rights reserved. 5 | # 6 | # This library is free software; you can redistribute it and/or 7 | # modify it under the terms of the GNU Lesser General Public 8 | # License as published by the Free Software Foundation; either 9 | # version 2.1 of the License, or (at your option) any later version. 10 | # 11 | # This library is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | # Lesser General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU Lesser General Public 17 | # License along with this library; if not, write to the Free Software 18 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 19 | # USA 20 | # 21 | import logging 22 | from functools import partial 23 | 24 | from dasbus.constants import DBUS_FLAG_NONE 25 | from dasbus.signal import Signal 26 | 27 | import gi 28 | gi.require_version("Gio", "2.0") 29 | from gi.repository import Gio 30 | 31 | log = logging.getLogger(__name__) 32 | 33 | __all__ = [ 34 | "DBusObserverError", 35 | "DBusObserver", 36 | "GLibMonitoring" 37 | ] 38 | 39 | 40 | class DBusObserverError(Exception): 41 | """Exception class for the DBus observers.""" 42 | pass 43 | 44 | 45 | class GLibMonitoring(object): 46 | """The low-level DBus monitoring library based on GLib.""" 47 | 48 | @classmethod 49 | def watch_name(cls, connection, name, flags=DBUS_FLAG_NONE, 50 | name_appeared=None, name_vanished=None): 51 | """Watch a service name on the DBus connection.""" 52 | name_appeared_closure = None 53 | name_vanished_closure = None 54 | 55 | if name_appeared: 56 | name_appeared_closure = partial( 57 | cls._name_appeared_callback, 58 | user_data=(name_appeared, ()) 59 | ) 60 | 61 | if name_vanished: 62 | name_vanished_closure = partial( 63 | cls._name_vanished_callback, 64 | user_data=(name_vanished, ()) 65 | ) 66 | 67 | registration_id = Gio.bus_watch_name_on_connection( 68 | connection, 69 | name, 70 | flags, 71 | name_appeared_closure, 72 | name_vanished_closure 73 | ) 74 | 75 | return partial( 76 | cls._unwatch_name, 77 | connection, 78 | registration_id 79 | ) 80 | 81 | @classmethod 82 | def _name_appeared_callback(cls, connection, name, name_owner, user_data): 83 | """Callback for watch_name..""" 84 | # Prepare the user's callback. 85 | callback, callback_args = user_data 86 | 87 | # Call user's callback. 88 | callback(name_owner, *callback_args) 89 | 90 | @classmethod 91 | def _name_vanished_callback(cls, connection, name, user_data): 92 | """Callback for watch_name.""" 93 | # Prepare the user's callback. 94 | callback, callback_args = user_data 95 | 96 | # Call user's callback. 97 | callback(*callback_args) 98 | 99 | @classmethod 100 | def _unwatch_name(cls, connection, registration_id): 101 | """Stops watching a service name on the DBus connection.""" 102 | Gio.bus_unwatch_name(registration_id) 103 | 104 | 105 | class DBusObserver(object): 106 | """Base class for DBus observers. 107 | 108 | This class is recommended to use only to watch the availability 109 | of a service on DBus. It doesn't provide any support for accessing 110 | objects provided by the service. 111 | 112 | Usage: 113 | 114 | .. code-block:: python 115 | 116 | # Create the observer and connect to its signals. 117 | observer = DBusObserver(SystemBus, "org.freedesktop.NetworkManager") 118 | 119 | def callback1(observer): 120 | print("Service is available!") 121 | 122 | def callback2(observer): 123 | print("Service is unavailable!") 124 | 125 | observer.service_available.connect(callback1) 126 | observer.service_unavailable.connect(callback2) 127 | 128 | # Connect to the service once it is available. 129 | observer.connect_once_available() 130 | 131 | # Disconnect the observer. 132 | observer.disconnect() 133 | 134 | """ 135 | 136 | def __init__(self, message_bus, service_name, monitoring=GLibMonitoring): 137 | """Creates a DBus service observer. 138 | 139 | :param message_bus: a message bus 140 | :param service_name: a DBus name of a service 141 | """ 142 | self._message_bus = message_bus 143 | self._service_name = service_name 144 | self._is_service_available = False 145 | 146 | self._service_available = Signal() 147 | self._service_unavailable = Signal() 148 | 149 | self._monitoring = monitoring 150 | self._subscriptions = [] 151 | 152 | @property 153 | def service_name(self): 154 | """Returns a DBus name.""" 155 | return self._service_name 156 | 157 | @property 158 | def is_service_available(self): 159 | """The proxy can be accessed.""" 160 | return self._is_service_available 161 | 162 | @property 163 | def service_available(self): 164 | """Signal that emits when the service is available. 165 | 166 | Signal emits this class as an argument. You have to 167 | call the watch method to activate the signals. 168 | """ 169 | return self._service_available 170 | 171 | @property 172 | def service_unavailable(self): 173 | """Signal that emits when the service is unavailable. 174 | 175 | Signal emits this class as an argument. You have to 176 | call the watch method to activate the signals. 177 | """ 178 | return self._service_unavailable 179 | 180 | def connect_once_available(self): 181 | """Connect to the service once it is available. 182 | 183 | The observer is not connected to the service until it 184 | emits the service_available signal. 185 | """ 186 | self._watch() 187 | 188 | def disconnect(self): 189 | """Disconnect from the service. 190 | 191 | Disconnect from the service if it is connected and stop 192 | watching its availability. 193 | """ 194 | self._unwatch() 195 | 196 | if self.is_service_available: 197 | self._disable_service() 198 | 199 | def _watch(self): 200 | """Watch the service name on DBus.""" 201 | subscription = self._monitoring.watch_name( 202 | self._message_bus.connection, 203 | self.service_name, 204 | DBUS_FLAG_NONE, 205 | self._service_name_appeared_callback, 206 | self._service_name_vanished_callback 207 | ) 208 | 209 | self._subscriptions.append(subscription) 210 | 211 | def _unwatch(self): 212 | """Stop to watch the service name on DBus.""" 213 | while self._subscriptions: 214 | callback = self._subscriptions.pop() 215 | callback() 216 | 217 | def _enable_service(self): 218 | """Enable the service.""" 219 | self._is_service_available = True 220 | self._service_available.emit(self) 221 | 222 | def _disable_service(self): 223 | """Disable the service.""" 224 | self._is_service_available = False 225 | self._service_unavailable.emit(self) 226 | 227 | def _service_name_appeared_callback(self, *args): 228 | """Callback for the watch method.""" 229 | if not self.is_service_available: 230 | self._enable_service() 231 | 232 | def _service_name_vanished_callback(self, *args): 233 | """Callback for the watch method.""" 234 | if self.is_service_available: 235 | self._disable_service() 236 | 237 | def __str__(self): 238 | """Returns a string version of this object.""" 239 | return self._service_name 240 | 241 | def __repr__(self): 242 | """Returns a string representation.""" 243 | return "{}({})".format( 244 | self.__class__.__name__, 245 | self._service_name 246 | ) 247 | -------------------------------------------------------------------------------- /src/dasbus/client/property.py: -------------------------------------------------------------------------------- 1 | # 2 | # Client support for DBus properties 3 | # 4 | # Copyright (C) 2019 Red Hat, Inc. All rights reserved. 5 | # 6 | # This library is free software; you can redistribute it and/or 7 | # modify it under the terms of the GNU Lesser General Public 8 | # License as published by the Free Software Foundation; either 9 | # version 2.1 of the License, or (at your option) any later version. 10 | # 11 | # This library is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | # Lesser General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU Lesser General Public 17 | # License along with this library; if not, write to the Free Software 18 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 19 | # USA 20 | # 21 | 22 | __all__ = ["PropertyProxy"] 23 | 24 | 25 | class PropertyProxy(object): 26 | """Proxy of a remote DBus property. 27 | 28 | It can be used to define instance attributes. 29 | """ 30 | 31 | __slots__ = [ 32 | "_getter", 33 | "_setter" 34 | ] 35 | 36 | def __init__(self, getter, setter): 37 | """Create a new proxy of the DBus property.""" 38 | self._getter = getter 39 | self._setter = setter 40 | 41 | def get(self): 42 | """Get the value of the DBus property.""" 43 | return self.__get__(None, None) # pylint: disable=unnecessary-dunder-call 44 | 45 | def __get__(self, instance, owner): 46 | if instance is None and owner: 47 | return self 48 | 49 | if not self._getter: 50 | raise AttributeError( 51 | "Can't read DBus property." 52 | ) 53 | 54 | return self._getter() 55 | 56 | def set(self, value): 57 | """Set the value of the DBus property.""" 58 | return self.__set__(None, value) # pylint: disable=unnecessary-dunder-call 59 | 60 | def __set__(self, instance, value): 61 | if not self._setter: 62 | raise AttributeError( 63 | "Can't set DBus property." 64 | ) 65 | 66 | return self._setter(value) 67 | -------------------------------------------------------------------------------- /src/dasbus/client/proxy.py: -------------------------------------------------------------------------------- 1 | # 2 | # Client support for DBus proxies 3 | # 4 | # Copyright (C) 2019 Red Hat, Inc. All rights reserved. 5 | # 6 | # This library is free software; you can redistribute it and/or 7 | # modify it under the terms of the GNU Lesser General Public 8 | # License as published by the Free Software Foundation; either 9 | # version 2.1 of the License, or (at your option) any later version. 10 | # 11 | # This library is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | # Lesser General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU Lesser General Public 17 | # License along with this library; if not, write to the Free Software 18 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 19 | # USA 20 | # 21 | from abc import ABCMeta, abstractmethod 22 | from threading import Lock 23 | 24 | from dasbus.client.handler import ClientObjectHandler 25 | from dasbus.client.property import PropertyProxy 26 | from dasbus.specification import DBusSpecificationError 27 | 28 | __all__ = [ 29 | "AbstractObjectProxy", 30 | "ObjectProxy", 31 | "InterfaceProxy", 32 | "get_object_path", 33 | "disconnect_proxy" 34 | ] 35 | 36 | 37 | def get_object_handler(proxy): 38 | """Get an object handler of the DBus proxy. 39 | 40 | :param proxy: a DBus proxy 41 | :return: a DBus proxy handler 42 | """ 43 | if not isinstance(proxy, AbstractObjectProxy): 44 | raise TypeError("Invalid type '{}'.".format(type(proxy).__name__)) 45 | 46 | return getattr(proxy, "_handler") 47 | 48 | 49 | def get_object_path(proxy): 50 | """Get an object path of the remote DBus object. 51 | 52 | :param proxy: a DBus proxy 53 | :return: a DBus path 54 | """ 55 | handler = get_object_handler(proxy) 56 | return handler.object_path 57 | 58 | 59 | def disconnect_proxy(proxy): 60 | """Disconnect the DBus proxy from the remote object. 61 | 62 | :param proxy: a DBus proxy 63 | """ 64 | handler = get_object_handler(proxy) 65 | handler.disconnect_members() 66 | 67 | 68 | class AbstractObjectProxy(metaclass=ABCMeta): 69 | """Abstract proxy of a remote DBus object.""" 70 | 71 | __slots__ = [ 72 | "_handler", 73 | "_members", 74 | "_lock", 75 | "__weakref__" 76 | ] 77 | 78 | # Set of local instance attributes. 79 | _locals = {*__slots__} 80 | 81 | def __init__(self, message_bus, service_name, object_path, 82 | handler_factory=ClientObjectHandler, **handler_arguments): 83 | """Create a new proxy. 84 | 85 | :param message_bus: a message bus 86 | :param service_name: a DBus name of the service 87 | :param object_path: a DBus path the object 88 | :param handler_factory: a factory of a DBus client object handler 89 | :param handler_arguments: additional arguments for the handler factory 90 | """ 91 | self._handler = handler_factory( 92 | message_bus, 93 | service_name, 94 | object_path, 95 | **handler_arguments 96 | ) 97 | self._members = {} 98 | self._lock = Lock() 99 | 100 | @abstractmethod 101 | def _get_interface(self, member_name): 102 | """Get the DBus interface of the member. 103 | 104 | :param member_name: a member name 105 | :return: an interface name 106 | """ 107 | pass 108 | 109 | def _get_member(self, *key): 110 | """Find a member of the DBus object. 111 | 112 | If the member doesn't exist, we will acquire 113 | a lock and ask a handler to create it. 114 | 115 | This method is thread-safe. 116 | 117 | :param key: a member key 118 | :return: a member 119 | :raise: AttributeError if invalid 120 | """ 121 | try: 122 | return self._members[key] 123 | except KeyError: 124 | pass 125 | 126 | return self._create_member(*key) 127 | 128 | def _create_member(self, *key): 129 | """Create a member of the DBus object. 130 | 131 | If the member doesn't exist, ask a handler 132 | to create it. 133 | 134 | This method is thread-safe. 135 | 136 | :param key: a member key 137 | :return: a member 138 | :raise: DBusSpecificationError if invalid 139 | """ 140 | with self._lock: 141 | try: 142 | return self._members[key] 143 | except KeyError: 144 | pass 145 | 146 | try: 147 | member = self._handler.create_member(*key) 148 | except DBusSpecificationError as e: 149 | raise AttributeError(str(e)) from None 150 | 151 | self._members[key] = member 152 | return member 153 | 154 | def __getattr__(self, name): 155 | """Get the attribute. 156 | 157 | Called when an attribute lookup has not found 158 | the attribute in the usual places. Always call 159 | the DBus handler in this case. 160 | """ 161 | member = self._get_member(self._get_interface(name), name) 162 | 163 | if isinstance(member, PropertyProxy): 164 | return member.get() 165 | 166 | return member 167 | 168 | def __setattr__(self, name, value): 169 | """Set the attribute. 170 | 171 | Called when an attribute assignment is attempted. 172 | Call the DBus handler if the name is not a 173 | name of an instance attribute defined in _locals. 174 | """ 175 | if name in self._locals: 176 | return super().__setattr__(name, value) 177 | 178 | member = self._get_member(self._get_interface(name), name) 179 | 180 | if isinstance(member, PropertyProxy): 181 | return member.set(value) 182 | 183 | raise AttributeError( 184 | "Can't set DBus attribute '{}'.".format(name) 185 | ) 186 | 187 | 188 | class ObjectProxy(AbstractObjectProxy): 189 | """Proxy of a remote DBus object.""" 190 | 191 | __slots__ = ["_interface_names"] 192 | 193 | # Set of instance attributes. 194 | _locals = {*AbstractObjectProxy._locals, *__slots__} 195 | 196 | def __init__(self, *args, **kwargs): 197 | """Create a new proxy. 198 | 199 | :param handler: a DBus client object handler 200 | """ 201 | super().__init__(*args, **kwargs) 202 | self._interface_names = None 203 | 204 | def _get_interface(self, member_name): 205 | """Get the DBus interface of the member. 206 | 207 | The members of standard interfaces have a priority. 208 | """ 209 | if self._interface_names is None: 210 | members = reversed( 211 | self._handler.specification.members 212 | ) 213 | self._interface_names = { 214 | m.name: m.interface_name 215 | for m in members 216 | } 217 | 218 | try: 219 | return self._interface_names[member_name] 220 | except KeyError: 221 | pass 222 | 223 | raise AttributeError( 224 | "DBus object has no attribute '{}'.".format(member_name) 225 | ) 226 | 227 | 228 | class InterfaceProxy(AbstractObjectProxy): 229 | """Proxy of a remote DBus interface.""" 230 | 231 | __slots__ = ["_interface_name"] 232 | 233 | # Set of instance attributes. 234 | _locals = {*AbstractObjectProxy._locals, *__slots__} 235 | 236 | def __init__(self, message_bus, service_name, object_path, 237 | interface_name, *args, **kwargs): 238 | """Create a new proxy. 239 | 240 | :param message_bus: a message bus 241 | :param service_name: a DBus name of the service 242 | :param object_path: a DBus path the object 243 | :param handler: a DBus client object handler 244 | """ 245 | super().__init__(message_bus, service_name, object_path, 246 | *args, **kwargs) 247 | self._interface_name = interface_name 248 | 249 | def _get_interface(self, member_name): 250 | """Get the DBus interface of the member.""" 251 | return self._interface_name 252 | -------------------------------------------------------------------------------- /src/dasbus/constants.py: -------------------------------------------------------------------------------- 1 | # 2 | # DBus constants 3 | # 4 | # Copyright (C) 2019 Red Hat, Inc. All rights reserved. 5 | # 6 | # This library is free software; you can redistribute it and/or 7 | # modify it under the terms of the GNU Lesser General Public 8 | # License as published by the Free Software Foundation; either 9 | # version 2.1 of the License, or (at your option) any later version. 10 | # 11 | # This library is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | # Lesser General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU Lesser General Public 17 | # License along with this library; if not, write to the Free Software 18 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 19 | # USA 20 | # 21 | 22 | # Status codes. 23 | DBUS_START_REPLY_SUCCESS = 1 24 | 25 | # No flags are set. 26 | DBUS_FLAG_NONE = 0 27 | 28 | # System environment variable holding the DBus session address. 29 | DBUS_STARTER_ADDRESS = "DBUS_STARTER_ADDRESS" 30 | 31 | # Return values of org.freedesktop.DBus.RequestName. 32 | DBUS_REQUEST_NAME_REPLY_PRIMARY_OWNER = 1 33 | DBUS_REQUEST_NAME_REPLY_IN_QUEUE = 2 34 | DBUS_REQUEST_NAME_REPLY_EXISTS = 3 35 | DBUS_REQUEST_NAME_REPLY_ALREADY_OWNER = 4 36 | 37 | # Flags of org.freedesktop.DBus.RequestName. 38 | DBUS_NAME_FLAG_ALLOW_REPLACEMENT = 0x1 39 | DBUS_NAME_FLAG_REPLACE_EXISTING = 0x2 40 | DBUS_NAME_FLAG_DO_NOT_QUEUE = 0x3 41 | -------------------------------------------------------------------------------- /src/dasbus/error.py: -------------------------------------------------------------------------------- 1 | # 2 | # Support for DBus errors 3 | # 4 | # Copyright (C) 2019 Red Hat, Inc. All rights reserved. 5 | # 6 | # This library is free software; you can redistribute it and/or 7 | # modify it under the terms of the GNU Lesser General Public 8 | # License as published by the Free Software Foundation; either 9 | # version 2.1 of the License, or (at your option) any later version. 10 | # 11 | # This library is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | # Lesser General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU Lesser General Public 17 | # License along with this library; if not, write to the Free Software 18 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 19 | # USA 20 | # 21 | from abc import ABCMeta, abstractmethod 22 | 23 | from dasbus.namespace import get_dbus_name 24 | 25 | __all__ = [ 26 | "get_error_decorator", 27 | "DBusError", 28 | "AbstractErrorRule", 29 | "ErrorRule", 30 | "DefaultErrorRule", 31 | "ErrorMapper" 32 | ] 33 | 34 | 35 | def get_error_decorator(error_mapper): 36 | """Generate a decorator for DBus errors. 37 | 38 | Create a function for decorating Python exception classes. 39 | The decorator will add a new rule to the given error mapper 40 | that will map the class to the specified error name. 41 | 42 | Definition of the decorator: 43 | 44 | .. code-block:: python 45 | 46 | decorator(error_name, namespace=()) 47 | 48 | The decorator accepts a name of the DBus error and optionally 49 | a namespace of the DBus name. The namespace will be used as 50 | a prefix of the DBus name. 51 | 52 | Usage: 53 | 54 | .. code-block:: python 55 | 56 | # Create an error mapper. 57 | error_mapper = ErrorMapper() 58 | 59 | # Create a decorator for DBus errors and use it to map 60 | # the class ExampleError to the name my.example.Error. 61 | dbus_error = create_error_decorator(error_mapper) 62 | 63 | @dbus_error("my.example.Error") 64 | class ExampleError(DBusError): 65 | pass 66 | 67 | :param error_mapper: an error mapper 68 | :return: a decorator 69 | """ 70 | def decorator(error_name, namespace=()): 71 | error_name = get_dbus_name(*namespace, error_name) 72 | 73 | def decorated(cls): 74 | error_mapper.add_rule(ErrorRule( 75 | exception_type=cls, 76 | error_name=error_name 77 | )) 78 | return cls 79 | 80 | return decorated 81 | 82 | return decorator 83 | 84 | 85 | class DBusError(Exception): 86 | """A default DBus error.""" 87 | pass 88 | 89 | 90 | class AbstractErrorRule(metaclass=ABCMeta): 91 | """Abstract rule for mapping a Python exception to a DBus error.""" 92 | 93 | __slots__ = [] 94 | 95 | @abstractmethod 96 | def match_type(self, exception_type): 97 | """Is this rule matching the given exception type? 98 | 99 | :param exception_type: a type of the Python error 100 | :return: True or False 101 | """ 102 | pass 103 | 104 | @abstractmethod 105 | def get_name(self, exception_type): 106 | """Get a DBus name for the given exception type. 107 | 108 | :param exception_type: a type of the Python error 109 | :return: a name of the DBus error 110 | """ 111 | pass 112 | 113 | @abstractmethod 114 | def match_name(self, error_name): 115 | """Is this rule matching the given DBus error? 116 | 117 | :param error_name: a name of the DBus error 118 | :return: True or False 119 | """ 120 | pass 121 | 122 | @abstractmethod 123 | def get_type(self, error_name): 124 | """Get an exception type of the given DBus error. 125 | 126 | param error_name: a name of the DBus error 127 | :return: a type of the Python error 128 | """ 129 | pass 130 | 131 | 132 | class ErrorRule(AbstractErrorRule): 133 | """Rule for mapping a Python exception to a DBus error.""" 134 | 135 | __slots__ = [ 136 | "_exception_type", 137 | "_error_name" 138 | ] 139 | 140 | def __init__(self, exception_type, error_name): 141 | """Create a new error rule. 142 | 143 | The rule will return the Python type exception_type 144 | for the DBue error error_name. 145 | 146 | The rule will return the DBue name error_name for 147 | the Python type exception_type 148 | 149 | :param exception_type: a type of the Python error 150 | :param error_name: a name of the DBus error 151 | """ 152 | self._exception_type = exception_type 153 | self._error_name = error_name 154 | 155 | def match_type(self, exception_type): 156 | """Is this rule matching the given exception type?""" 157 | return self._exception_type == exception_type 158 | 159 | def get_name(self, exception_type): 160 | """Get a DBus name for the given exception type.""" 161 | return self._error_name 162 | 163 | def match_name(self, error_name): 164 | """Is this rule matching the given DBus error?""" 165 | return self._error_name == error_name 166 | 167 | def get_type(self, error_name): 168 | """Get an exception type of the given DBus error.""" 169 | return self._exception_type 170 | 171 | 172 | class DefaultErrorRule(AbstractErrorRule): 173 | """Default rule for mapping a Python exception to a DBus error.""" 174 | 175 | __slots__ = [ 176 | "_default_type", 177 | "_default_namespace" 178 | ] 179 | 180 | def __init__(self, default_type, default_namespace): 181 | """Create a new default rule. 182 | 183 | The rule will return the Python type default_type 184 | for the all DBus errors. 185 | 186 | The rule will generate a DBus name with the prefix 187 | default_namespace for all Python exception types. 188 | 189 | :param default_type: a default type of the Python error 190 | :param default_namespace: a default namespace of the DBus error 191 | """ 192 | self._default_type = default_type 193 | self._default_namespace = default_namespace 194 | 195 | def match_type(self, exception_type): 196 | """Is this rule matching the given exception type?""" 197 | return True 198 | 199 | def get_name(self, exception_type): 200 | """Get a DBus name for the given exception type.""" 201 | return get_dbus_name(*self._default_namespace, exception_type.__name__) 202 | 203 | def match_name(self, error_name): 204 | """Is this rule matching the given DBus error?""" 205 | return True 206 | 207 | def get_type(self, error_name): 208 | """Get an exception type of the given DBus error.""" 209 | return self._default_type 210 | 211 | 212 | class ErrorMapper(object): 213 | """Class for mapping Python exceptions to DBus errors.""" 214 | 215 | __slots__ = ["_error_rules"] 216 | 217 | def __init__(self): 218 | """Create a new error mapper.""" 219 | self._error_rules = [] 220 | self.reset_rules() 221 | 222 | def add_rule(self, rule: AbstractErrorRule): 223 | """Add a rule to the error mapper. 224 | 225 | The new rule will have a higher priority than 226 | the rules already contained in the error mapper. 227 | 228 | :param rule: an error rule 229 | :type rule: an instance of AbstractErrorRule 230 | """ 231 | self._error_rules.append(rule) 232 | 233 | def reset_rules(self): 234 | """Reset rules in the error mapper. 235 | 236 | Reset the error rules to the initial state. 237 | All rules will be replaced with the default ones. 238 | """ 239 | # Clear the list. 240 | self._error_rules = [] 241 | 242 | # Add the default rules. 243 | self.add_rule(DefaultErrorRule( 244 | default_type=DBusError, 245 | default_namespace=("not", "known", "Error") 246 | )) 247 | 248 | def get_error_name(self, exception_type): 249 | """Get a DBus name of the Python exception. 250 | 251 | Try to find a matching rule in the error mapper. 252 | If a rule matches the given exception type, use 253 | the rule to get the name of the DBus error. 254 | 255 | The rules in the error mapper are processed in 256 | the reversed order to respect the priority of 257 | the rules. 258 | 259 | :param exception_type: a type of the Python error 260 | :type exception_type: a subclass of Exception 261 | :return: a name of the DBus error 262 | :raise LookupError: if no name is found 263 | """ 264 | for rule in reversed(self._error_rules): 265 | if rule.match_type(exception_type): 266 | return rule.get_name(exception_type) 267 | 268 | raise LookupError( 269 | "No name found for '{}'.".format(exception_type.__name__) 270 | ) 271 | 272 | def get_exception_type(self, error_name): 273 | """Get a Python exception type of the DBus error. 274 | 275 | Try to find a matching rule in the error mapper. 276 | If a rule matches the given name of a DBus error, 277 | use the rule to get the type of a Python exception. 278 | 279 | The rules in the error mapper are processed in 280 | the reversed order to respect the priority of 281 | the rules. 282 | 283 | :param error_name: a name of the DBus error 284 | :return: a type of the Python exception 285 | :rtype: a subclass of Exception 286 | :raise LookupError: if no type is found 287 | """ 288 | for rule in reversed(self._error_rules): 289 | if rule.match_name(error_name): 290 | return rule.get_type(error_name) 291 | 292 | raise LookupError("No type found for '{}'.".format(error_name)) 293 | -------------------------------------------------------------------------------- /src/dasbus/identifier.py: -------------------------------------------------------------------------------- 1 | # 2 | # Identification of DBus objects, interfaces and services 3 | # 4 | # Copyright (C) 2019 Red Hat, Inc. All rights reserved. 5 | # 6 | # This library is free software; you can redistribute it and/or 7 | # modify it under the terms of the GNU Lesser General Public 8 | # License as published by the Free Software Foundation; either 9 | # version 2.1 of the License, or (at your option) any later version. 10 | # 11 | # This library is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | # Lesser General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU Lesser General Public 17 | # License along with this library; if not, write to the Free Software 18 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 19 | # USA 20 | # 21 | from dasbus.namespace import get_dbus_path, get_dbus_name 22 | 23 | __all__ = [ 24 | 'DBusInterfaceIdentifier', 25 | 'DBusObjectIdentifier', 26 | 'DBusServiceIdentifier' 27 | ] 28 | 29 | 30 | class DBusBaseIdentifier(object): 31 | """A base identifier.""" 32 | 33 | def __init__(self, namespace, basename=None): 34 | """Create an identifier. 35 | 36 | :param namespace: a sequence of strings 37 | :param basename: a string with the base name or None 38 | """ 39 | if basename: 40 | namespace = (*namespace, basename) 41 | 42 | self._namespace = namespace 43 | self._name = get_dbus_name(*namespace) 44 | self._path = get_dbus_path(*namespace) 45 | 46 | @property 47 | def namespace(self): 48 | """DBus namespace of this object.""" 49 | return self._namespace 50 | 51 | def __str__(self): 52 | """Return the string representation.""" 53 | return self._name 54 | 55 | 56 | class DBusInterfaceIdentifier(DBusBaseIdentifier): 57 | """Identifier of a DBus interface.""" 58 | 59 | def __init__(self, namespace, basename=None, interface_version=None): 60 | """Describe a DBus interface. 61 | 62 | :param namespace: a sequence of strings 63 | :param basename: a string with the base name or None 64 | :param interface_version: a version of the interface 65 | """ 66 | super().__init__(namespace, basename=basename) 67 | self._interface_version = interface_version 68 | 69 | def _version_to_string(self, version): 70 | """Convert version to a string. 71 | 72 | :param version: a number or None 73 | :return: a string 74 | """ 75 | if version is None: 76 | return "" 77 | 78 | return str(version) 79 | 80 | @property 81 | def interface_name(self): 82 | """Full name of the DBus interface.""" 83 | return self._name + self._version_to_string(self._interface_version) 84 | 85 | def __str__(self): 86 | """Return the string representation.""" 87 | return self.interface_name 88 | 89 | 90 | class DBusObjectIdentifier(DBusInterfaceIdentifier): 91 | """Identifier of a DBus object.""" 92 | 93 | def __init__(self, namespace, basename=None, interface_version=None, 94 | object_version=None): 95 | """Describe a DBus object. 96 | 97 | :param namespace: a sequence of strings 98 | :param basename: a string with the base name or None 99 | :param interface_version: a version of the DBus interface 100 | :param object_version: a version of the DBus object 101 | """ 102 | super().__init__(namespace, basename=basename, 103 | interface_version=interface_version) 104 | self._object_version = object_version 105 | 106 | @property 107 | def object_path(self): 108 | """Full path of the DBus object.""" 109 | return self._path + self._version_to_string(self._object_version) 110 | 111 | def __str__(self): 112 | """Return the string representation.""" 113 | return self.object_path 114 | 115 | 116 | class DBusServiceIdentifier(DBusObjectIdentifier): 117 | """Identifier of a DBus service.""" 118 | 119 | def __init__(self, message_bus, namespace, basename=None, 120 | interface_version=None, object_version=None, 121 | service_version=None): 122 | """Describe a DBus service. 123 | 124 | :param message_bus: a message bus 125 | :param namespace: a sequence of strings 126 | :param basename: a string with the base name or None 127 | :param interface_version: a version of the DBus interface 128 | :param object_version: a version of the DBus object 129 | :param service_version: a version of the DBus service 130 | """ 131 | super().__init__(namespace, basename=basename, 132 | interface_version=interface_version, 133 | object_version=object_version) 134 | 135 | self._service_version = service_version 136 | self._message_bus = message_bus 137 | 138 | @property 139 | def message_bus(self): 140 | """Message bus of the DBus service. 141 | 142 | :return: a message bus 143 | :rtype: an instance of the MessageBus class 144 | """ 145 | return self._message_bus 146 | 147 | @property 148 | def service_name(self): 149 | """Full name of a DBus service.""" 150 | return self._name + self._version_to_string(self._service_version) 151 | 152 | def __str__(self): 153 | """Return the string representation.""" 154 | return self.service_name 155 | 156 | def _choose_object_path(self, object_id): 157 | """Choose an object path.""" 158 | if object_id is None: 159 | return self.object_path 160 | 161 | if isinstance(object_id, DBusObjectIdentifier): 162 | return object_id.object_path 163 | 164 | return object_id 165 | 166 | def _choose_interface_name(self, interface_id): 167 | """Choose an interface name.""" 168 | if interface_id is None: 169 | return None 170 | 171 | if isinstance(interface_id, DBusInterfaceIdentifier): 172 | return interface_id.interface_name 173 | 174 | return interface_id 175 | 176 | def get_proxy(self, object_path=None, interface_name=None, 177 | **bus_arguments): 178 | """Returns a proxy of the DBus object. 179 | 180 | If no object path is specified, we will use the object path 181 | of this DBus service. 182 | 183 | If no interface name is specified, we will use none and create 184 | a proxy from all interfaces of the DBus object. 185 | 186 | :param object_path: an object identifier or a DBus path or None 187 | :param interface_name: an interface identifier or a DBus name or None 188 | :param bus_arguments: additional arguments for the message bus 189 | :return: a proxy object 190 | """ 191 | object_path = self._choose_object_path(object_path) 192 | interface_name = self._choose_interface_name(interface_name) 193 | 194 | return self._message_bus.get_proxy( 195 | self.service_name, 196 | object_path, 197 | interface_name, 198 | **bus_arguments 199 | ) 200 | -------------------------------------------------------------------------------- /src/dasbus/loop.py: -------------------------------------------------------------------------------- 1 | # 2 | # Representation of an event loop 3 | # 4 | # Copyright (C) 2020 Red Hat, Inc. All rights reserved. 5 | # 6 | # This library is free software; you can redistribute it and/or 7 | # modify it under the terms of the GNU Lesser General Public 8 | # License as published by the Free Software Foundation; either 9 | # version 2.1 of the License, or (at your option) any later version. 10 | # 11 | # This library is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | # Lesser General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU Lesser General Public 17 | # License along with this library; if not, write to the Free Software 18 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 19 | # USA 20 | # 21 | from abc import ABCMeta, abstractmethod 22 | 23 | import gi 24 | gi.require_version("GLib", "2.0") 25 | from gi.repository import GLib 26 | 27 | __all__ = [ 28 | "AbstractEventLoop", 29 | "EventLoop" 30 | ] 31 | 32 | 33 | class AbstractEventLoop(metaclass=ABCMeta): 34 | """The abstract representation of the event loop. 35 | 36 | It is necessary to run the event loop to handle emitted 37 | DBus signals or incoming DBus calls (in the DBus service). 38 | 39 | Example: 40 | 41 | .. code-block:: python 42 | 43 | # Create the event loop. 44 | loop = EventLoop() 45 | 46 | # Start the event loop. 47 | loop.run() 48 | 49 | # Run loop.quit() to stop. 50 | 51 | """ 52 | 53 | @abstractmethod 54 | def run(self): 55 | """Start the event loop.""" 56 | pass 57 | 58 | @abstractmethod 59 | def quit(self): 60 | """Stop the event loop.""" 61 | pass 62 | 63 | 64 | class EventLoop(AbstractEventLoop): 65 | """The representation of the event loop.""" 66 | 67 | def __init__(self): 68 | """Create the event loop.""" 69 | self._loop = GLib.MainLoop() 70 | 71 | def run(self): 72 | """Start the event loop.""" 73 | self._loop.run() 74 | 75 | def quit(self): 76 | """Stop the event loop.""" 77 | self._loop.quit() 78 | -------------------------------------------------------------------------------- /src/dasbus/namespace.py: -------------------------------------------------------------------------------- 1 | # 2 | # DBus names and paths 3 | # 4 | # Copyright (C) 2019 Red Hat, Inc. All rights reserved. 5 | # 6 | # This library is free software; you can redistribute it and/or 7 | # modify it under the terms of the GNU Lesser General Public 8 | # License as published by the Free Software Foundation; either 9 | # version 2.1 of the License, or (at your option) any later version. 10 | # 11 | # This library is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | # Lesser General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU Lesser General Public 17 | # License along with this library; if not, write to the Free Software 18 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 19 | # USA# 20 | 21 | __all__ = [ 22 | "get_dbus_name", 23 | "get_dbus_path", 24 | "get_namespace_from_name" 25 | ] 26 | 27 | 28 | def get_dbus_name(*namespace): 29 | """Create a DBus name from the given names. 30 | 31 | :param namespace: a sequence of names 32 | :return: a DBus name 33 | """ 34 | return ".".join(namespace) 35 | 36 | 37 | def get_dbus_path(*namespace): 38 | """Create a DBus path from the given names. 39 | 40 | :param namespace: a sequence of names 41 | :return: a DBus path 42 | """ 43 | return "/" + "/".join(namespace) 44 | 45 | 46 | def get_namespace_from_name(name): 47 | """Return a namespace of the DBus name. 48 | 49 | :param name: a DBus name 50 | :return: a sequence of names 51 | """ 52 | return tuple(name.split(".")) 53 | -------------------------------------------------------------------------------- /src/dasbus/server/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dasbus-project/dasbus/be51b94b083bad6fa0716ad6dc97d12f4462f8d4/src/dasbus/server/__init__.py -------------------------------------------------------------------------------- /src/dasbus/server/container.py: -------------------------------------------------------------------------------- 1 | # 2 | # Server support for DBus containers 3 | # 4 | # Copyright (C) 2019 Red Hat, Inc. All rights reserved. 5 | # 6 | # This library is free software; you can redistribute it and/or 7 | # modify it under the terms of the GNU Lesser General Public 8 | # License as published by the Free Software Foundation; either 9 | # version 2.1 of the License, or (at your option) any later version. 10 | # 11 | # This library is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | # Lesser General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU Lesser General Public 17 | # License along with this library; if not, write to the Free Software 18 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 19 | # USA 20 | # 21 | from dasbus.namespace import get_dbus_path 22 | from dasbus.server.publishable import Publishable 23 | from dasbus.typing import ObjPath, List 24 | 25 | __all__ = [ 26 | "DBusContainerError", 27 | "DBusContainer" 28 | ] 29 | 30 | 31 | class DBusContainerError(Exception): 32 | """General exception for DBus container errors.""" 33 | pass 34 | 35 | 36 | class DBusContainer(object): 37 | """The container of DBus objects. 38 | 39 | A DBus container should be used to dynamically publish Publishable 40 | objects within the same namespace. It generates a unique DBus path 41 | for each object. It is able to resolve a DBus path into an object 42 | and an object into a DBus path. 43 | 44 | Example: 45 | 46 | .. code-block:: python 47 | 48 | # Create a container of tasks. 49 | container = DBusContainer( 50 | namespace=("my", "project"), 51 | basename="Task", 52 | message_bus=DBus 53 | ) 54 | 55 | # Publish a task. 56 | path = container.to_object_path(MyTask()) 57 | 58 | # Resolve an object path into a task. 59 | task = container.from_object_path(path) 60 | 61 | """ 62 | 63 | def __init__(self, message_bus, namespace, basename=None): 64 | """Create a new container. 65 | 66 | :param message_bus: a message bus 67 | :param namespace: a sequence of names 68 | :param basename: a string with the base name 69 | """ 70 | self._message_bus = message_bus 71 | 72 | if basename: 73 | namespace = (*namespace, basename) 74 | 75 | self._namespace = namespace[:-1] 76 | self._basename = namespace[-1] 77 | 78 | self._container = {} 79 | self._published = set() 80 | self._counter = 0 81 | 82 | def set_namespace(self, namespace): 83 | """Set the namespace. 84 | 85 | All DBus objects from the container should use the same 86 | namespace, so the namespace should be set up before any 87 | of the DBus objects are published. 88 | 89 | :param namespace: a sequence of names 90 | """ 91 | self._namespace = namespace 92 | 93 | def from_object_path(self, object_path: ObjPath): 94 | """Convert a DBus path to a published object. 95 | 96 | If no published object is found for the given DBus path, 97 | raise DBusContainerError. 98 | 99 | :param object_path: a DBus path 100 | :return: a published object 101 | """ 102 | return self._find_object(object_path) 103 | 104 | def to_object_path(self, obj) -> ObjPath: 105 | """Convert a publishable object to a DBus path. 106 | 107 | If no DBus path is found for the given object, publish 108 | the object on the container message bus with a unique 109 | DBus path generated from the container namespace. 110 | 111 | :param obj: a publishable object 112 | :return: a DBus path 113 | """ 114 | if not isinstance(obj, Publishable): 115 | raise TypeError( 116 | "Type '{}' is not publishable.".format(type(obj).__name__) 117 | ) 118 | 119 | if not self._is_object_published(obj): 120 | self._publish_object(obj) 121 | 122 | return self._find_object_path(obj) 123 | 124 | def from_object_path_list(self, object_paths: List[ObjPath]): 125 | """Convert DBus paths to published objects. 126 | 127 | :param object_paths: a list of DBus paths 128 | :return: a list of published objects 129 | """ 130 | return list(map(self.from_object_path, object_paths)) 131 | 132 | def to_object_path_list(self, objects) -> List[ObjPath]: 133 | """Convert publishable objects to DBus paths. 134 | 135 | :param objects: a list of publishable objects 136 | :return: a list of DBus paths 137 | """ 138 | return list(map(self.to_object_path, objects)) 139 | 140 | def _is_object_published(self, obj): 141 | """Is the given object published? 142 | 143 | :param obj: an object 144 | :return: True if the object is published, otherwise False 145 | """ 146 | return id(obj) in self._published 147 | 148 | def _publish_object(self, obj: Publishable): 149 | """Publish the given object. 150 | 151 | :param obj: an object to publish 152 | :return: an object path 153 | """ 154 | object_path = self._generate_object_path() 155 | 156 | self._message_bus.publish_object( 157 | object_path, 158 | obj.for_publication() 159 | ) 160 | 161 | self._container[object_path] = obj 162 | self._published.add(id(obj)) 163 | return object_path 164 | 165 | def _find_object_path(self, obj): 166 | """Find a DBus path of the object. 167 | 168 | :param obj: a published object 169 | :return: a DBus path 170 | :raise: DBusContainerError if no object path is found 171 | """ 172 | for object_path, found_obj in self._container.items(): 173 | if found_obj is obj: 174 | return object_path 175 | 176 | raise DBusContainerError( 177 | "No object path found." 178 | ) 179 | 180 | def _find_object(self, object_path): 181 | """Find an object by its DBus path. 182 | 183 | :param object_path: a DBus path 184 | :return: a published object 185 | :raise: DBusContainerError if no object is found 186 | """ 187 | if object_path in self._container: 188 | return self._container[object_path] 189 | 190 | raise DBusContainerError( 191 | "Unknown object path '{}'.".format(object_path) 192 | ) 193 | 194 | def _generate_object_path(self): 195 | """Generate a unique object path. 196 | 197 | This method is not thread safe. 198 | 199 | :return: a unique object path 200 | """ 201 | self._counter += 1 202 | 203 | return get_dbus_path( 204 | *self._namespace, 205 | self._basename, 206 | str(self._counter) 207 | ) 208 | -------------------------------------------------------------------------------- /src/dasbus/server/property.py: -------------------------------------------------------------------------------- 1 | # 2 | # Server support for DBus properties 3 | # 4 | # Copyright (C) 2019 Red Hat, Inc. All rights reserved. 5 | # 6 | # This library is free software; you can redistribute it and/or 7 | # modify it under the terms of the GNU Lesser General Public 8 | # License as published by the Free Software Foundation; either 9 | # version 2.1 of the License, or (at your option) any later version. 10 | # 11 | # This library is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | # Lesser General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU Lesser General Public 17 | # License along with this library; if not, write to the Free Software 18 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 19 | # USA 20 | # 21 | # For more info about DBus specification see: 22 | # https://dbus.freedesktop.org/doc/dbus-specification.html#introspection-format 23 | # 24 | from abc import ABCMeta 25 | from collections import defaultdict 26 | from functools import wraps 27 | from typing import Dict 28 | 29 | from dasbus.server.interface import dbus_signal, get_xml 30 | from dasbus.specification import DBusSpecification, DBusSpecificationError 31 | from dasbus.typing import get_variant, Str, Variant, List 32 | 33 | __all__ = [ 34 | "emits_properties_changed", 35 | "PropertiesException", 36 | "PropertiesInterface" 37 | ] 38 | 39 | 40 | def emits_properties_changed(method): 41 | """Decorator for emitting properties changes. 42 | 43 | The decorated method has to be a member of a class that 44 | inherits PropertiesInterface. 45 | 46 | :param method: a DBus method of a class that inherits PropertiesInterface 47 | :return: a wrapper of a DBus method that emits PropertiesChanged 48 | """ 49 | @wraps(method) 50 | def wrapper(obj, *args, **kwargs): 51 | result = method(obj, *args, **kwargs) 52 | obj.flush_changes() 53 | return result 54 | 55 | return wrapper 56 | 57 | 58 | class PropertiesException(Exception): 59 | """Exception for DBus properties.""" 60 | pass 61 | 62 | 63 | class PropertiesChanges(object): 64 | """Cache for properties changes. 65 | 66 | This class is useful to collect the changed properties 67 | and their values, before they are emitted on DBus. 68 | """ 69 | 70 | def __init__(self, obj): 71 | """Create the cache. 72 | 73 | :param obj: an object with DBus properties 74 | """ 75 | self._object = obj 76 | self._properties_names = set() 77 | self._properties_specs = self._find_properties_specs(obj) 78 | 79 | def _find_properties_specs(self, obj): 80 | """Find specifications of DBus properties. 81 | 82 | :param obj: an object with DBus properties 83 | :return: a map of property names and their specifications 84 | """ 85 | specification = DBusSpecification.from_xml(get_xml(obj)) 86 | properties_specs = {} 87 | 88 | for member in specification.members: 89 | if not isinstance(member, DBusSpecification.Property): 90 | continue 91 | 92 | if member.name in properties_specs: 93 | raise DBusSpecificationError( 94 | "DBus property '{}' is defined in more than " 95 | "one interface.".format(member.name) 96 | ) 97 | 98 | properties_specs[member.name] = member 99 | 100 | return properties_specs 101 | 102 | def flush(self): 103 | """Flush the cache. 104 | 105 | The content of the cache will be composed to requests 106 | and the cache will be cleared. 107 | 108 | The requests can be used to emit the PropertiesChanged 109 | signal. The requests are a list of tuples, that contain 110 | an interface name and a dictionary of properties changes. 111 | 112 | :return: a list of requests 113 | """ 114 | content = self._properties_names 115 | self._properties_names = set() 116 | requests = defaultdict(dict) 117 | 118 | for property_name in content: 119 | # Find the property specification. 120 | member = self._properties_specs[property_name] 121 | 122 | # Get the property value. 123 | value = getattr(self._object, property_name) 124 | variant = get_variant(member.type, value) 125 | 126 | # Create a request. 127 | requests[member.interface_name][member.name] = variant 128 | 129 | return requests.items() 130 | 131 | def check_property(self, property_name): 132 | """Check if the property name is valid.""" 133 | if property_name not in self._properties_specs: 134 | raise PropertiesException( 135 | "DBus object has no property '{}'.".format( 136 | property_name 137 | ) 138 | ) 139 | 140 | def update(self, property_name): 141 | """Update the cache.""" 142 | self.check_property(property_name) 143 | self._properties_names.add(property_name) 144 | 145 | 146 | class PropertiesInterface(metaclass=ABCMeta): 147 | """Standard DBus interface org.freedesktop.DBus.Properties. 148 | 149 | DBus objects don't have to inherit this class, because the 150 | DBus library provides support for this interface by default. 151 | This class only extends this support. 152 | 153 | Report the changed property: 154 | 155 | .. code-block:: python 156 | 157 | self.report_changed_property('X') 158 | 159 | Emit all changes when the method is done: 160 | 161 | .. code-block:: python 162 | 163 | @emits_properties_changed 164 | def SetX(x: Int): 165 | self.set_x(x) 166 | 167 | """ 168 | 169 | def __init__(self): 170 | """Initialize the interface.""" 171 | self._properties_changes = PropertiesChanges(self) 172 | 173 | @dbus_signal 174 | def PropertiesChanged(self, interface: Str, changed: Dict[Str, Variant], 175 | invalid: List[Str]): 176 | """Standard signal properties changed. 177 | 178 | :param interface: a name of an interface 179 | :param changed: a dictionary of changed properties 180 | :param invalid: a list of invalidated properties 181 | :return: 182 | """ 183 | pass 184 | 185 | def report_changed_property(self, property_name): 186 | """Reports changed DBus property. 187 | 188 | :param property_name: a name of a DBus property 189 | """ 190 | self._properties_changes.update(property_name) 191 | 192 | def flush_changes(self): 193 | """Flush properties changes.""" 194 | requests = self._properties_changes.flush() 195 | 196 | for interface, changes in requests: 197 | self.PropertiesChanged(interface, changes, []) 198 | -------------------------------------------------------------------------------- /src/dasbus/server/publishable.py: -------------------------------------------------------------------------------- 1 | # 2 | # Server support for publishable Python objects 3 | # 4 | # Copyright (C) 2019 Red Hat, Inc. All rights reserved. 5 | # 6 | # This library is free software; you can redistribute it and/or 7 | # modify it under the terms of the GNU Lesser General Public 8 | # License as published by the Free Software Foundation; either 9 | # version 2.1 of the License, or (at your option) any later version. 10 | # 11 | # This library is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | # Lesser General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU Lesser General Public 17 | # License along with this library; if not, write to the Free Software 18 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 19 | # USA 20 | # 21 | from abc import ABCMeta, abstractmethod 22 | 23 | __all__ = ["Publishable"] 24 | 25 | 26 | class Publishable(metaclass=ABCMeta): 27 | """Abstract class for Python objects that can be published on DBus. 28 | 29 | Example: 30 | 31 | .. code-block:: python 32 | 33 | # Define a publishable class. 34 | class MyObject(Publishable): 35 | 36 | def for_publication(self): 37 | return MyDBusInterface(self) 38 | 39 | # Create a publishable object. 40 | my_object = MyObject() 41 | 42 | # Publish the object on DBus. 43 | DBus.publish_object("/org/project/x", my_object.for_publication()) 44 | 45 | """ 46 | 47 | @abstractmethod 48 | def for_publication(self): 49 | """Return a DBus representation of this object. 50 | 51 | :return: an instance of @dbus_interface or @dbus_class 52 | """ 53 | return None 54 | -------------------------------------------------------------------------------- /src/dasbus/server/template.py: -------------------------------------------------------------------------------- 1 | # 2 | # Templates for DBus interfaces 3 | # 4 | # Copyright (C) 2019 Red Hat, Inc. All rights reserved. 5 | # 6 | # This library is free software; you can redistribute it and/or 7 | # modify it under the terms of the GNU Lesser General Public 8 | # License as published by the Free Software Foundation; either 9 | # version 2.1 of the License, or (at your option) any later version. 10 | # 11 | # This library is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | # Lesser General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU Lesser General Public 17 | # License along with this library; if not, write to the Free Software 18 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 19 | # USA 20 | # 21 | from abc import ABCMeta 22 | 23 | from dasbus.server.property import PropertiesInterface 24 | 25 | __all__ = [ 26 | "BasicInterfaceTemplate", 27 | "InterfaceTemplate" 28 | ] 29 | 30 | 31 | class BasicInterfaceTemplate(metaclass=ABCMeta): 32 | """Basic template for a DBus interface. 33 | 34 | This template uses a software design pattern called proxy. 35 | 36 | This class provides a recommended way how to define DBus interfaces 37 | and create publishable DBus objects. The class that defines a DBus 38 | interface should inherit this class and be decorated with @dbus_class 39 | or @dbus_interface decorator. The implementation of this interface will 40 | be provided by a separate object called implementation. Therefore the 41 | methods of this class should call the methods of the implementation, 42 | the signals should be connected to the signals of the implementation 43 | and the getters and setters of properties should access the properties 44 | of the implementation. 45 | 46 | .. code-block:: python 47 | 48 | @dbus_interface("org.myproject.X") 49 | class InterfaceX(BasicInterfaceTemplate): 50 | def DoSomething(self) -> Str: 51 | return self.implementation.do_something() 52 | 53 | class X(object): 54 | def do_something(self): 55 | return "Done!" 56 | 57 | x = X() 58 | i = InterfaceX(x) 59 | 60 | DBus.publish_object("/org/myproject/X", i) 61 | 62 | """ 63 | 64 | def __init__(self, implementation): 65 | """Create a publishable DBus object. 66 | 67 | :param implementation: an implementation of this interface 68 | """ 69 | self._implementation = implementation 70 | self.connect_signals() 71 | 72 | @property 73 | def implementation(self): 74 | """Return the implementation of this interface. 75 | 76 | :return: an implementation 77 | """ 78 | return self._implementation 79 | 80 | def connect_signals(self): 81 | """Interconnect the signals. 82 | 83 | You should connect the emit methods of the interface 84 | signals to the signals of the implementation. Every 85 | time the implementation emits a signal, this interface 86 | reemits the signal on DBus. 87 | """ 88 | pass 89 | 90 | 91 | class InterfaceTemplate(BasicInterfaceTemplate, PropertiesInterface): 92 | """Template for a DBus interface. 93 | 94 | The interface provides the support for the standard interface 95 | org.freedesktop.DBus.Properties. 96 | 97 | Usage: 98 | 99 | .. code-block:: python 100 | 101 | def connect_signals(self): 102 | super().connect_signals() 103 | self.implementation.module_properties_changed.connect( 104 | self.flush_changes 105 | ) 106 | self.watch_property("X", self.implementation.x_changed) 107 | 108 | @property 109 | def X(self, x) -> Int: 110 | return self.implementation.x 111 | 112 | @emits_properties_changed 113 | def SetX(self, x: Int): 114 | self.implementation.set_x(x) 115 | 116 | """ 117 | 118 | def __init__(self, implementation): 119 | PropertiesInterface.__init__(self) 120 | BasicInterfaceTemplate.__init__(self, implementation) 121 | 122 | def watch_property(self, property_name, signal): 123 | """Watch a DBus property. 124 | 125 | Report a change when the property is changed. 126 | 127 | :param property_name: a name of a DBus property 128 | :param signal: a signal that emits when the property is changed 129 | """ 130 | self._properties_changes.check_property(property_name) 131 | 132 | def callback(*args, **kwargs): 133 | self.report_changed_property(property_name) 134 | 135 | signal.connect(callback) 136 | -------------------------------------------------------------------------------- /src/dasbus/signal.py: -------------------------------------------------------------------------------- 1 | # 2 | # Representation of a signal 3 | # 4 | # Copyright (C) 2019 Red Hat, Inc. All rights reserved. 5 | # 6 | # This library is free software; you can redistribute it and/or 7 | # modify it under the terms of the GNU Lesser General Public 8 | # License as published by the Free Software Foundation; either 9 | # version 2.1 of the License, or (at your option) any later version. 10 | # 11 | # This library is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | # Lesser General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU Lesser General Public 17 | # License along with this library; if not, write to the Free Software 18 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 19 | # USA 20 | # 21 | 22 | __all__ = ["Signal"] 23 | 24 | 25 | class Signal(object): 26 | """Default representation of a signal.""" 27 | 28 | __slots__ = [ 29 | "_callbacks", 30 | "__weakref__" 31 | ] 32 | 33 | def __init__(self): 34 | """Create a new signal.""" 35 | self._callbacks = [] 36 | 37 | def connect(self, callback): 38 | """Connect to a signal. 39 | 40 | :param callback: a function to register 41 | """ 42 | self._callbacks.append(callback) 43 | 44 | def __call__(self, *args, **kwargs): 45 | """Emit a signal with the given arguments.""" 46 | self.emit(*args, **kwargs) 47 | 48 | def emit(self, *args, **kwargs): 49 | """Emit a signal with the given arguments.""" 50 | # The list of callbacks can be changed, so 51 | # use a copy of the list for the iteration. 52 | for callback in self._callbacks.copy(): 53 | callback(*args, **kwargs) 54 | 55 | def disconnect(self, callback=None): 56 | """Disconnect from a signal. 57 | 58 | If no callback is specified, then all functions will 59 | be unregistered from the signal. 60 | 61 | If the specified callback isn't registered, do nothing. 62 | 63 | :param callback: a function to unregister or None 64 | """ 65 | if callback is None: 66 | self._callbacks.clear() 67 | return 68 | 69 | try: 70 | self._callbacks.remove(callback) 71 | except ValueError: 72 | pass 73 | -------------------------------------------------------------------------------- /src/dasbus/specification.py: -------------------------------------------------------------------------------- 1 | # 2 | # Support for DBus XML specifications 3 | # 4 | # Copyright (C) 2019 Red Hat, Inc. All rights reserved. 5 | # 6 | # This library is free software; you can redistribute it and/or 7 | # modify it under the terms of the GNU Lesser General Public 8 | # License as published by the Free Software Foundation; either 9 | # version 2.1 of the License, or (at your option) any later version. 10 | # 11 | # This library is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | # Lesser General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU Lesser General Public 17 | # License along with this library; if not, write to the Free Software 18 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 19 | # USA 20 | # 21 | # For more info about DBus specification see: 22 | # https://dbus.freedesktop.org/doc/dbus-specification.html#introspection-format 23 | # 24 | from collections import namedtuple 25 | 26 | from dasbus.xml import XMLParser 27 | 28 | __all__ = [ 29 | "DBusSpecificationError", 30 | "DBusSpecification", 31 | "DBusSpecificationParser" 32 | ] 33 | 34 | 35 | class DBusSpecificationError(Exception): 36 | """Exception for the DBus specification errors.""" 37 | pass 38 | 39 | 40 | class DBusSpecification(object): 41 | """DBus XML specification.""" 42 | 43 | DIRECTION_IN = "in" 44 | DIRECTION_OUT = "out" 45 | ACCESS_READ = "read" 46 | ACCESS_WRITE = "write" 47 | ACCESS_READWRITE = "readwrite" 48 | RETURN_PARAMETER = "return" 49 | 50 | STANDARD_INTERFACES = """ 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | """ 86 | 87 | # Representation of specification members. 88 | Signal = namedtuple("Signal", [ 89 | "name", 90 | "interface_name", 91 | "type" 92 | ]) 93 | 94 | Method = namedtuple("Method", [ 95 | "name", 96 | "interface_name", 97 | "in_type", 98 | "out_type" 99 | ]) 100 | 101 | Property = namedtuple("Property", [ 102 | "name", 103 | "interface_name", 104 | "readable", 105 | "writable", 106 | "type" 107 | ]) 108 | 109 | # Specification data holders. 110 | __slots__ = ["_members"] 111 | 112 | @classmethod 113 | def from_xml(cls, xml): 114 | """Return a DBus specification for the given XML.""" 115 | return DBusSpecificationParser.parse_specification(xml, cls) 116 | 117 | def __init__(self): 118 | """Create a new DBus specification.""" 119 | self._members = {} 120 | 121 | @property 122 | def interfaces(self): 123 | """Interfaces of the DBus specification.""" 124 | return list(dict(self._members.keys()).keys()) 125 | 126 | @property 127 | def members(self): 128 | """Members of the DBus specification.""" 129 | return list(self._members.values()) 130 | 131 | def add_member(self, member): 132 | """Add a member of a DBus interface.""" 133 | self._members[(member.interface_name, member.name)] = member 134 | 135 | def get_member(self, interface_name, member_name): 136 | """Get a member of a DBus interface.""" 137 | try: 138 | return self._members[(interface_name, member_name)] 139 | except KeyError: 140 | pass 141 | 142 | raise DBusSpecificationError( 143 | "DBus specification has no member '{}.{}'.".format( 144 | interface_name, member_name 145 | ) 146 | ) 147 | 148 | 149 | class DBusSpecificationParser(object): 150 | """Class for parsing DBus XML specification.""" 151 | 152 | # The XML parser. 153 | xml_parser = XMLParser 154 | 155 | @classmethod 156 | def parse_specification(cls, xml, factory=DBusSpecification): 157 | """Generate a representation of a DBus XML specification. 158 | 159 | :param xml: the XML specification to parse 160 | :param factory: the DBus specification factory 161 | :return: a representation od the DBus specification 162 | """ 163 | specification = factory() 164 | cls._parse_xml(specification, DBusSpecification.STANDARD_INTERFACES) 165 | cls._parse_xml(specification, xml) 166 | return specification 167 | 168 | @classmethod 169 | def _parse_xml(cls, specification, xml): 170 | """Parse the given XML.""" 171 | node = cls.xml_parser.xml_to_element(xml) 172 | 173 | # Iterate over interfaces. 174 | for interface_element in node: 175 | if not cls.xml_parser.is_interface(interface_element): 176 | continue 177 | 178 | # Parse the interface. 179 | cls._parse_interface(specification, interface_element) 180 | 181 | @classmethod 182 | def _parse_interface(cls, specification, interface_element): 183 | """Parse the interface element from the DBus specification.""" 184 | interface_name = cls.xml_parser.get_name(interface_element) 185 | 186 | # Iterate over members. 187 | for member_element in interface_element: 188 | 189 | if cls.xml_parser.is_property(member_element): 190 | member = cls._parse_property(interface_name, member_element) 191 | 192 | elif cls.xml_parser.is_signal(member_element): 193 | member = cls._parse_signal(interface_name, member_element) 194 | 195 | elif cls.xml_parser.is_method(member_element): 196 | member = cls._parse_method(interface_name, member_element) 197 | 198 | else: 199 | continue 200 | 201 | # Add the member specification to the mapping. 202 | specification.add_member(member) 203 | 204 | return interface_name 205 | 206 | @classmethod 207 | def _parse_property(cls, interface_name, property_element): 208 | """Parse the property element from the DBus specification.""" 209 | property_name = cls.xml_parser.get_name(property_element) 210 | property_type = cls.xml_parser.get_type(property_element) 211 | property_access = cls.xml_parser.get_access(property_element) 212 | 213 | readable = property_access in ( 214 | DBusSpecification.ACCESS_READ, 215 | DBusSpecification.ACCESS_READWRITE 216 | ) 217 | 218 | writable = property_access in ( 219 | DBusSpecification.ACCESS_WRITE, 220 | DBusSpecification.ACCESS_READWRITE 221 | ) 222 | 223 | return DBusSpecification.Property( 224 | name=property_name, 225 | interface_name=interface_name, 226 | readable=readable, 227 | writable=writable, 228 | type=property_type 229 | ) 230 | 231 | @classmethod 232 | def _parse_signal(cls, interface_name, signal_element): 233 | """Parse the signal element from the DBus specification.""" 234 | signal_name = cls.xml_parser.get_name(signal_element) 235 | signal_type = [] 236 | 237 | for element in signal_element: 238 | if not cls.xml_parser.is_parameter(element): 239 | continue 240 | 241 | element_type = cls.xml_parser.get_type(element) 242 | signal_type.append(element_type) 243 | 244 | return DBusSpecification.Signal( 245 | name=signal_name, 246 | interface_name=interface_name, 247 | type=cls._get_type(signal_type) 248 | ) 249 | 250 | @classmethod 251 | def _parse_method(cls, interface_name, method_element): 252 | """Parse the method element from the DBus specification.""" 253 | method_name = cls.xml_parser.get_name(method_element) 254 | in_types = [] 255 | out_types = [] 256 | 257 | for element in method_element: 258 | if not cls.xml_parser.is_parameter(element): 259 | continue 260 | 261 | direction = cls.xml_parser.get_direction(element) 262 | element_type = cls.xml_parser.get_type(element) 263 | 264 | if direction == DBusSpecification.DIRECTION_IN: 265 | in_types.append(element_type) 266 | 267 | elif direction == DBusSpecification.DIRECTION_OUT: 268 | out_types.append(element_type) 269 | 270 | return DBusSpecification.Method( 271 | name=method_name, 272 | interface_name=interface_name, 273 | in_type=cls._get_type(in_types), 274 | out_type=cls._get_type(out_types) 275 | ) 276 | 277 | @classmethod 278 | def _get_type(cls, types): 279 | """Join types into one value.""" 280 | if not types: 281 | return None 282 | 283 | return "({})".format("".join(types)) 284 | -------------------------------------------------------------------------------- /src/dasbus/unix.py: -------------------------------------------------------------------------------- 1 | # 2 | # Support for Unix file descriptors. 3 | # 4 | # Copyright (C) 2022 Red Hat, Inc. All rights reserved. 5 | # 6 | # This library is free software; you can redistribute it and/or 7 | # modify it under the terms of the GNU Lesser General Public 8 | # License as published by the Free Software Foundation; either 9 | # version 2.1 of the License, or (at your option) any later version. 10 | # 11 | # This library is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | # Lesser General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU Lesser General Public 17 | # License along with this library; if not, write to the Free Software 18 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 19 | # USA 20 | # 21 | import logging 22 | 23 | from dasbus.constants import DBUS_FLAG_NONE 24 | from dasbus.typing import VariantUnpacking, get_variant 25 | from dasbus.client.handler import GLibClient 26 | from dasbus.server.handler import GLibServer 27 | 28 | import gi 29 | gi.require_version("Gio", "2.0") 30 | from gi.repository import Gio 31 | 32 | log = logging.getLogger(__name__) 33 | 34 | __all__ = [ 35 | "GLibClientUnix", 36 | "GLibServerUnix", 37 | ] 38 | 39 | 40 | def acquire_fds(variant): 41 | """Acquire Unix file descriptors contained in a variant. 42 | 43 | Return a variant with indexes into a list of Unix file descriptors 44 | and the list of Unix file descriptors. 45 | 46 | If the variant is None, or the variant doesn't contain any Unix 47 | file descriptors, return None instead of the list. 48 | 49 | :param variant: a variant with Unix file descriptors 50 | :return: a variant with indexes and a list of Unix file descriptors 51 | """ 52 | if variant is None: 53 | return None, None 54 | 55 | fd_list = [] 56 | 57 | def _get_idx(fd): 58 | fd_list.append(fd) 59 | return len(fd_list) - 1 60 | 61 | variant_without_fds = UnixFDSwap.apply(variant, _get_idx) 62 | 63 | if not fd_list: 64 | return variant, None 65 | 66 | return variant_without_fds, Gio.UnixFDList.new_from_array(fd_list) 67 | 68 | 69 | def restore_fds(variant, fd_list: Gio.UnixFDList): 70 | """Restore Unix file descriptors in a variant. 71 | 72 | If the variant is None, return None. Otherwise, return 73 | a variant with Unix file descriptors. 74 | 75 | :param variant: a variant with indexes into fd_list 76 | :param fd_list: a list of Unix file descriptors 77 | :return: a variant with Unix file descriptors 78 | """ 79 | if variant is None: 80 | return None 81 | 82 | if fd_list is None: 83 | return variant 84 | 85 | fd_list = fd_list.steal_fds() 86 | 87 | if not fd_list: 88 | return variant 89 | 90 | def _get_fd(index): 91 | try: 92 | return fd_list[index] 93 | except IndexError: 94 | return -1 95 | 96 | return UnixFDSwap.apply(variant, _get_fd) 97 | 98 | 99 | class UnixFDSwap(VariantUnpacking): 100 | """Class for swapping values of the UnixFD type.""" 101 | 102 | @classmethod 103 | def apply(cls, variant, swap): 104 | """Swap unix file descriptors with indices. 105 | 106 | The provided function should swap a unix file 107 | descriptor with an index into an array of unix 108 | file descriptors or vice versa. 109 | 110 | :param variant: a variant to modify 111 | :param swap: a swapping function 112 | :return: a modified variant 113 | """ 114 | return cls._recreate_variant(variant, swap) 115 | 116 | @classmethod 117 | def _handle_variant(cls, variant, *extras): 118 | """Handle a variant.""" 119 | return cls._recreate_variant(variant.get_variant(), *extras) 120 | 121 | @classmethod 122 | def _handle_value(cls, variant, *extras): 123 | """Handle a basic value.""" 124 | type_string = variant.get_type_string() 125 | 126 | # Handle the unix file descriptor. 127 | if type_string == 'h': 128 | # Get the swapping function. 129 | swap, *_ = extras 130 | # Swap the values. 131 | return swap(variant.get_handle()) 132 | 133 | return variant.unpack() 134 | 135 | @classmethod 136 | def _recreate_variant(cls, variant, *extras): 137 | """Create a variant with swapped values.""" 138 | type_string = variant.get_type_string() 139 | 140 | # Do nothing if there is no unix file descriptor to handle. 141 | if 'h' not in type_string and 'v' not in type_string: 142 | return variant 143 | 144 | # Get a new value of the variant. 145 | value = cls._process_variant(variant, *extras) 146 | 147 | # Create a new variant. 148 | return get_variant(type_string, value) 149 | 150 | 151 | class GLibClientUnix(GLibClient): 152 | """The low-level DBus client library based on GLib.""" 153 | 154 | @classmethod 155 | def sync_call(cls, connection, service_name, object_path, interface_name, 156 | method_name, parameters, reply_type, flags=DBUS_FLAG_NONE, 157 | timeout=GLibClient.DBUS_TIMEOUT_NONE): 158 | """Synchronously call a DBus method. 159 | 160 | :return: a result of the DBus call 161 | """ 162 | # Process Unix file descriptors in parameters. 163 | parameters, fd_list = acquire_fds(parameters) 164 | 165 | # Call the DBus method. 166 | result = connection.call_with_unix_fd_list_sync( 167 | service_name, 168 | object_path, 169 | interface_name, 170 | method_name, 171 | parameters, 172 | reply_type, 173 | flags, 174 | timeout, 175 | fd_list, 176 | None 177 | ) 178 | 179 | # Restore Unix file descriptors in the result. 180 | return restore_fds(*result) 181 | 182 | @classmethod 183 | def async_call(cls, connection, service_name, object_path, interface_name, 184 | method_name, parameters, reply_type, callback, 185 | callback_args=(), flags=DBUS_FLAG_NONE, 186 | timeout=GLibClient.DBUS_TIMEOUT_NONE): 187 | """Asynchronously call a DBus method.""" 188 | # Process Unix file descriptors in parameters. 189 | parameters, fd_list = acquire_fds(parameters) 190 | 191 | # Call the DBus method. 192 | connection.call_with_unix_fd_list( 193 | service_name, 194 | object_path, 195 | interface_name, 196 | method_name, 197 | parameters, 198 | reply_type, 199 | flags, 200 | timeout, 201 | fd_list, 202 | callback=cls._async_call_finish, 203 | user_data=(callback, callback_args) 204 | ) 205 | 206 | @classmethod 207 | def _async_call_finish(cls, source_object, result_object, user_data): 208 | """Finish an asynchronous DBus method call.""" 209 | # Prepare the user's callback. 210 | callback, callback_args = user_data 211 | 212 | def _finish_call(): 213 | # Retrieve the result of the call. 214 | result = source_object.call_with_unix_fd_list_finish( 215 | result_object 216 | ) 217 | # Restore Unix file descriptors in the result. 218 | return restore_fds(*result) 219 | 220 | # Call user's callback. 221 | callback(_finish_call, *callback_args) 222 | 223 | 224 | class GLibServerUnix(GLibServer): 225 | """The low-level DBus server library based on GLib. 226 | 227 | Adds Unix FD Support to base class""" 228 | 229 | @classmethod 230 | def emit_signal(cls, connection, object_path, interface_name, 231 | signal_name, parameters, destination=None): 232 | """Emit a DBus signal. 233 | 234 | GLib doesn't seem to support Unix file descriptors in signals. 235 | Swap Unix file descriptors with indexes into a list of Unix file 236 | descriptors, but emit just the indexes. Log a warning to inform 237 | users about the limited support. 238 | """ 239 | # Process Unix file descriptors in parameters. 240 | parameters, fd_list = acquire_fds(parameters) 241 | 242 | if fd_list: 243 | log.warning("Unix file descriptors in signals are unsupported.") 244 | 245 | # Emit the signal without Unix file descriptors. 246 | connection.emit_signal( 247 | destination, 248 | object_path, 249 | interface_name, 250 | signal_name, 251 | parameters 252 | ) 253 | 254 | @classmethod 255 | def set_call_reply(cls, invocation, out_type, out_value): 256 | """Set the reply of the DBus call.""" 257 | # Process Unix file descriptors in the reply. 258 | reply_value = cls._get_reply_value(out_type, out_value) 259 | reply_args = acquire_fds(reply_value) 260 | 261 | # Send the reply. 262 | invocation.return_value_with_unix_fd_list(*reply_args) 263 | 264 | @classmethod 265 | def _object_callback(cls, connection, sender, object_path, 266 | interface_name, method_name, parameters, 267 | invocation, user_data): 268 | """A method call closure of a DBus object.""" 269 | # Prepare the user's callback. 270 | callback, callback_args = user_data 271 | 272 | # Restore Unix file descriptors in parameters. 273 | fd_list = invocation.get_message().get_unix_fd_list() 274 | parameters = restore_fds(parameters, fd_list) 275 | 276 | # Call user's callback. 277 | callback( 278 | invocation, 279 | interface_name, 280 | method_name, 281 | parameters, 282 | *callback_args 283 | ) 284 | -------------------------------------------------------------------------------- /src/dasbus/xml.py: -------------------------------------------------------------------------------- 1 | # 2 | # Support for XML representation 3 | # 4 | # Copyright (C) 2019 Red Hat, Inc. All rights reserved. 5 | # 6 | # This library is free software; you can redistribute it and/or 7 | # modify it under the terms of the GNU Lesser General Public 8 | # License as published by the Free Software Foundation; either 9 | # version 2.1 of the License, or (at your option) any later version. 10 | # 11 | # This library is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | # Lesser General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU Lesser General Public 17 | # License along with this library; if not, write to the Free Software 18 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 19 | # USA 20 | # 21 | # For more info about DBus specification see: 22 | # https://dbus.freedesktop.org/doc/dbus-specification.html#introspection-format 23 | # 24 | from xml.etree import ElementTree 25 | from xml.dom import minidom 26 | 27 | __all__ = [ 28 | "XMLParser", 29 | "XMLGenerator" 30 | ] 31 | 32 | 33 | class XMLParser(object): 34 | """Class for parsing XML.""" 35 | 36 | @staticmethod 37 | def xml_to_element(xml): 38 | return ElementTree.fromstring(xml) 39 | 40 | @staticmethod 41 | def is_member(member_node): 42 | return member_node.tag in ("method", "signal", "property") 43 | 44 | @staticmethod 45 | def is_interface(member_node): 46 | return member_node.tag == "interface" 47 | 48 | @staticmethod 49 | def is_signal(member_node): 50 | return member_node.tag == "signal" 51 | 52 | @staticmethod 53 | def is_method(member_node): 54 | return member_node.tag == "method" 55 | 56 | @staticmethod 57 | def is_property(member_node): 58 | return member_node.tag == "property" 59 | 60 | @staticmethod 61 | def is_parameter(member_node): 62 | return member_node.tag == "arg" 63 | 64 | @staticmethod 65 | def has_name(node, node_name): 66 | return node.attrib.get("name", "") == node_name 67 | 68 | @staticmethod 69 | def get_name(node): 70 | return node.attrib["name"] 71 | 72 | @staticmethod 73 | def get_type(node): 74 | return node.attrib["type"] 75 | 76 | @staticmethod 77 | def get_access(node): 78 | return node.attrib["access"] 79 | 80 | @staticmethod 81 | def get_direction(node): 82 | return node.attrib["direction"] 83 | 84 | @staticmethod 85 | def get_interfaces_from_node(node_element): 86 | """Return a dictionary of interfaces defined in a node element.""" 87 | interfaces = {} 88 | 89 | for element in node_element.iterfind("interface"): 90 | interfaces[element.attrib["name"]] = element 91 | 92 | return interfaces 93 | 94 | 95 | class XMLGenerator(XMLParser): 96 | """Class for generating XML.""" 97 | 98 | @staticmethod 99 | def element_to_xml(element): 100 | """Return XML of the element.""" 101 | return ElementTree.tostring( 102 | element, 103 | method="xml", 104 | encoding="unicode" 105 | ) 106 | 107 | @staticmethod 108 | def prettify_xml(xml): 109 | """Return pretty printed normalized XML. 110 | 111 | Python 3.8 changed the order of the attributes and introduced 112 | the function canonicalize that should be used to normalize XML. 113 | """ 114 | # Remove newlines and extra whitespaces, 115 | xml_line = "".join([line.strip() for line in xml.splitlines()]) 116 | 117 | # Generate pretty xml. 118 | xml = minidom.parseString(xml_line).toprettyxml(indent=" ") 119 | 120 | # Normalize attributes. 121 | canonicalize = getattr( 122 | ElementTree, "canonicalize", lambda xml, *args, **kwargs: xml 123 | ) 124 | 125 | return canonicalize(xml, with_comments=True) 126 | 127 | @staticmethod 128 | def add_child(parent_element, child_element): 129 | """Append the child element to the parent element.""" 130 | parent_element.append(child_element) 131 | 132 | @staticmethod 133 | def add_comment(element, comment): 134 | element.append(ElementTree.Comment(text=comment)) 135 | 136 | @staticmethod 137 | def create_node(): 138 | """Create a node element called node.""" 139 | return ElementTree.Element("node") 140 | 141 | @staticmethod 142 | def create_interface(name): 143 | """Create an interface element.""" 144 | return ElementTree.Element("interface", {"name": name}) 145 | 146 | @staticmethod 147 | def create_signal(name): 148 | """Create a signal element.""" 149 | return ElementTree.Element("signal", {"name": name}) 150 | 151 | @staticmethod 152 | def create_method(name): 153 | """Create a method element.""" 154 | return ElementTree.Element("method", {"name": name}) 155 | 156 | @staticmethod 157 | def create_parameter(name, param_type, direction): 158 | """Create a parameter element.""" 159 | tag = "arg" 160 | attr = { 161 | "name": name, 162 | "type": param_type, 163 | "direction": direction 164 | } 165 | return ElementTree.Element(tag, attr) 166 | 167 | @staticmethod 168 | def create_property(name, property_type, access): 169 | """Create a property element.""" 170 | tag = "property" 171 | attr = { 172 | "name": name, 173 | "type": property_type, 174 | "access": access 175 | } 176 | return ElementTree.Element(tag, attr) 177 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dasbus-project/dasbus/be51b94b083bad6fa0716ad6dc97d12f4462f8d4/tests/__init__.py -------------------------------------------------------------------------------- /tests/lib_dbus.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2022 Red Hat, Inc. All rights reserved. 3 | # 4 | # This library is free software; you can redistribute it and/or 5 | # modify it under the terms of the GNU Lesser General Public 6 | # License as published by the Free Software Foundation; either 7 | # version 2.1 of the License, or (at your option) any later version. 8 | # 9 | # This library is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | # Lesser General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU Lesser General Public 15 | # License along with this library; if not, write to the Free Software 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 17 | # USA 18 | # 19 | import multiprocessing 20 | import sys 21 | import unittest 22 | 23 | from abc import abstractmethod, ABCMeta 24 | from contextlib import contextmanager 25 | from threading import Thread 26 | 27 | import gi 28 | gi.require_version("Gio", "2.0") 29 | gi.require_version("GLib", "2.0") 30 | from gi.repository import GLib, Gio 31 | 32 | 33 | def run_loop(timeout=3): 34 | """Run an event loop for the specified timeout. 35 | 36 | If any of the events fail or there are some pending 37 | events after the timeout, raise AssertionError. 38 | 39 | :param int timeout: a number of seconds 40 | """ 41 | loop = GLib.MainLoop() 42 | 43 | def _kill_loop(): 44 | loop.quit() 45 | return False 46 | 47 | GLib.timeout_add_seconds(timeout, _kill_loop) 48 | 49 | with catch_errors() as errors: 50 | loop.run() 51 | 52 | assert not errors, "The loop has failed!" 53 | assert not loop.get_context().pending() 54 | 55 | 56 | @contextmanager 57 | def catch_errors(): 58 | """Catch exceptions raised in this context. 59 | 60 | :return: a list of exceptions 61 | """ 62 | errors = [] 63 | 64 | def _handle_error(*exc_info): 65 | errors.append(exc_info) 66 | sys.__excepthook__(*exc_info) 67 | 68 | try: 69 | sys.excepthook = _handle_error 70 | yield errors 71 | finally: 72 | sys.excepthook = sys.__excepthook__ 73 | 74 | 75 | class AbstractDBusTestCase(unittest.TestCase, metaclass=ABCMeta): 76 | """Test DBus support with a real DBus connection.""" 77 | 78 | def setUp(self): 79 | """Set up the test.""" 80 | self.maxDiff = None 81 | 82 | # Initialize the service and the clients. 83 | self.service = self._get_service() 84 | self.clients = [] 85 | 86 | # Start a testing bus. 87 | self.bus = Gio.TestDBus() 88 | self.bus.up() 89 | 90 | # Create a connection to the testing bus. 91 | self.bus_address = self.bus.get_bus_address() 92 | self.message_bus = self._get_message_bus( 93 | self.bus_address 94 | ) 95 | 96 | @abstractmethod 97 | def _get_service(self): 98 | """Get a service.""" 99 | return None 100 | 101 | @classmethod 102 | @abstractmethod 103 | def _get_message_bus(cls, bus_address): 104 | """Get a testing message bus.""" 105 | return None 106 | 107 | @classmethod 108 | def _get_service_proxy(cls, message_bus, **proxy_args): 109 | """Get a proxy of the example service.""" 110 | return message_bus.get_proxy( 111 | "my.testing.Example", 112 | "/my/testing/Example", 113 | **proxy_args 114 | ) 115 | 116 | def _add_client(self, callback, *args, **kwargs): 117 | """Add a client.""" 118 | self.clients.append(callback) 119 | 120 | def _publish_service(self): 121 | """Publish the service on DBus.""" 122 | self.message_bus.publish_object( 123 | "/my/testing/Example", 124 | self.service 125 | ) 126 | self.message_bus.register_service( 127 | "my.testing.Example" 128 | ) 129 | 130 | def _run_test(self): 131 | """Run a test.""" 132 | self._publish_service() 133 | 134 | for client in self.clients: 135 | client.start() 136 | 137 | run_loop() 138 | 139 | for client in self.clients: 140 | client.join() 141 | 142 | def tearDown(self): 143 | """Tear down the test.""" 144 | if self.message_bus: 145 | self.message_bus.disconnect() 146 | 147 | if self.bus: 148 | self.bus.down() 149 | 150 | 151 | class DBusThreadedTestCase(AbstractDBusTestCase, metaclass=ABCMeta): 152 | """Test DBus support with a real DBus connection and threads.""" 153 | 154 | def _add_client(self, callback, *args, **kwargs): 155 | """Add a client thread.""" 156 | thread = Thread( 157 | target=callback, 158 | args=args, 159 | kwargs=kwargs, 160 | daemon=True, 161 | ) 162 | super()._add_client(thread) 163 | 164 | 165 | class DBusSpawnedTestCase(AbstractDBusTestCase, metaclass=ABCMeta): 166 | """Test DBus support with a real DBus connections and spawned processes.""" 167 | 168 | def setUp(self): 169 | """Set up the test.""" 170 | super().setUp() 171 | self.context = multiprocessing.get_context('spawn') 172 | 173 | def _add_client(self, callback, *args, **kwargs): 174 | """Add a client process.""" 175 | process = self.context.Process( 176 | name=callback.__name__, 177 | target=callback, 178 | args=(self.bus_address, *args), 179 | kwargs=kwargs, 180 | daemon=True, 181 | ) 182 | super()._add_client(process) 183 | 184 | def _run_test(self): 185 | """Run a test.""" 186 | super()._run_test() 187 | 188 | # Check the exit codes of the clients. 189 | for client in self.clients: 190 | msg = "{} has finished with {}".format( 191 | client.name, 192 | client.exitcode 193 | ) 194 | self.assertEqual(client.exitcode, 0, msg) 195 | -------------------------------------------------------------------------------- /tests/test_container.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2019 Red Hat, Inc. All rights reserved. 3 | # 4 | # This library is free software; you can redistribute it and/or 5 | # modify it under the terms of the GNU Lesser General Public 6 | # License as published by the Free Software Foundation; either 7 | # version 2.1 of the License, or (at your option) any later version. 8 | # 9 | # This library is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | # Lesser General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU Lesser General Public 15 | # License along with this library; if not, write to the Free Software 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 17 | # USA 18 | # 19 | import unittest 20 | from unittest.mock import Mock 21 | 22 | from dasbus.server.container import DBusContainer, DBusContainerError 23 | from dasbus.server.interface import dbus_interface 24 | from dasbus.server.publishable import Publishable 25 | from dasbus.server.template import BasicInterfaceTemplate 26 | from dasbus.typing import Str, ObjPath 27 | 28 | 29 | @dbus_interface("org.Project.Object") 30 | class MyInterface(BasicInterfaceTemplate): 31 | 32 | def HelloWorld(self) -> Str: 33 | return self.implementation.hello_world() 34 | 35 | 36 | class MyObject(Publishable): 37 | 38 | def for_publication(self): 39 | return MyInterface(self) 40 | 41 | def hello_world(self): 42 | return "Hello World!" 43 | 44 | 45 | class MyUnpublishable(object): 46 | pass 47 | 48 | 49 | class DBusContainerTestCase(unittest.TestCase): 50 | """Test DBus containers.""" 51 | 52 | def setUp(self): 53 | self.message_bus = Mock() 54 | self.container = DBusContainer( 55 | namespace=("org", "Project"), 56 | basename="Object", 57 | message_bus=self.message_bus 58 | ) 59 | 60 | def test_set_namespace(self): 61 | """Test set_namespace.""" 62 | self.container.set_namespace(("org", "Another", "Project")) 63 | 64 | path = self.container.to_object_path(MyObject()) 65 | self.assertEqual(path, "/org/Another/Project/Object/1") 66 | 67 | path = self.container.to_object_path(MyObject()) 68 | self.assertEqual(path, "/org/Another/Project/Object/2") 69 | 70 | def test_to_object_path_failed(self): 71 | """Test failed to_object_path.""" 72 | with self.assertRaises(TypeError) as cm: 73 | self.container.to_object_path(MyUnpublishable()) 74 | 75 | self.assertEqual( 76 | "Type 'MyUnpublishable' is not publishable.", 77 | str(cm.exception) 78 | ) 79 | 80 | with self.assertRaises(DBusContainerError) as cm: 81 | self.container._find_object_path(MyObject()) 82 | 83 | self.assertEqual( 84 | "No object path found.", 85 | str(cm.exception) 86 | ) 87 | 88 | def test_to_object_path(self): 89 | """Test to_object_path.""" 90 | obj = MyObject() 91 | path = self.container.to_object_path(obj) 92 | 93 | self.message_bus.publish_object.assert_called_once() 94 | published_path, published_obj = \ 95 | self.message_bus.publish_object.call_args[0] 96 | 97 | self.assertEqual(path, "/org/Project/Object/1") 98 | self.assertEqual(path, published_path) 99 | self.assertIsInstance(published_obj, MyInterface) 100 | self.assertEqual(obj, published_obj.implementation) 101 | 102 | self.message_bus.reset_mock() 103 | 104 | self.assertEqual(self.container.to_object_path(obj), path) 105 | self.message_bus.publish_object.assert_not_called() 106 | 107 | self.assertEqual(self.container.to_object_path(obj), path) 108 | self.message_bus.publish_object.assert_not_called() 109 | 110 | def test_to_object_path_list(self): 111 | """Test to_object_path_list.""" 112 | objects = [MyObject(), MyObject(), MyObject()] 113 | paths = self.container.to_object_path_list(objects) 114 | 115 | self.assertEqual(self.message_bus.publish_object.call_count, 3) 116 | 117 | self.assertEqual(paths, [ 118 | "/org/Project/Object/1", 119 | "/org/Project/Object/2", 120 | "/org/Project/Object/3" 121 | ]) 122 | 123 | self.message_bus.reset_mock() 124 | 125 | self.assertEqual(paths, self.container.to_object_path_list(objects)) 126 | self.message_bus.publish_object.assert_not_called() 127 | 128 | self.assertEqual(paths, self.container.to_object_path_list(objects)) 129 | self.message_bus.publish_object.assert_not_called() 130 | 131 | def test_from_object_path_failed(self): 132 | """Test failures.""" 133 | with self.assertRaises(DBusContainerError) as cm: 134 | self.container.from_object_path(ObjPath("/org/Project/Object/1")) 135 | 136 | self.assertEqual( 137 | "Unknown object path '/org/Project/Object/1'.", 138 | str(cm.exception) 139 | ) 140 | 141 | def test_from_object_path(self): 142 | """Test from_object_path.""" 143 | obj = MyObject() 144 | path = self.container.to_object_path(obj) 145 | 146 | self.assertEqual(obj, self.container.from_object_path(path)) 147 | self.assertEqual(path, self.container.to_object_path(obj)) 148 | 149 | self.assertEqual(obj, self.container.from_object_path(path)) 150 | self.assertEqual(path, self.container.to_object_path(obj)) 151 | 152 | def test_from_object_path_list(self): 153 | """Test from_object_path_list.""" 154 | objects = [MyObject(), MyObject(), MyObject()] 155 | paths = self.container.to_object_path_list(objects) 156 | 157 | self.assertEqual(objects, self.container.from_object_path_list(paths)) 158 | self.assertEqual(paths, self.container.to_object_path_list(objects)) 159 | 160 | self.assertEqual(objects, self.container.from_object_path_list(paths)) 161 | self.assertEqual(paths, self.container.to_object_path_list(objects)) 162 | 163 | def test_multiple_objects(self): 164 | """Test multiple objects.""" 165 | obj = MyObject() 166 | path = self.container.to_object_path(obj) 167 | self.assertEqual(path, "/org/Project/Object/1") 168 | self.assertEqual(obj, self.container.from_object_path(path)) 169 | self.message_bus.publish_object.assert_called_once() 170 | self.message_bus.reset_mock() 171 | 172 | obj = MyObject() 173 | path = self.container.to_object_path(obj) 174 | self.assertEqual(path, "/org/Project/Object/2") 175 | self.assertEqual(obj, self.container.from_object_path(path)) 176 | self.message_bus.publish_object.assert_called_once() 177 | self.message_bus.reset_mock() 178 | 179 | obj = MyObject() 180 | path = self.container.to_object_path(obj) 181 | self.assertEqual(path, "/org/Project/Object/3") 182 | self.assertEqual(obj, self.container.from_object_path(path)) 183 | self.message_bus.publish_object.assert_called_once() 184 | self.message_bus.reset_mock() 185 | -------------------------------------------------------------------------------- /tests/test_error.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2019 Red Hat, Inc. All rights reserved. 3 | # 4 | # This library is free software; you can redistribute it and/or 5 | # modify it under the terms of the GNU Lesser General Public 6 | # License as published by the Free Software Foundation; either 7 | # version 2.1 of the License, or (at your option) any later version. 8 | # 9 | # This library is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | # Lesser General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU Lesser General Public 15 | # License along with this library; if not, write to the Free Software 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 17 | # USA 18 | # 19 | import unittest 20 | 21 | from dasbus.error import ErrorMapper, DBusError, get_error_decorator, ErrorRule 22 | 23 | 24 | class ExceptionA(Exception): 25 | """My testing exception A.""" 26 | pass 27 | 28 | 29 | class ExceptionA1(ExceptionA): 30 | """My testing exception A1.""" 31 | pass 32 | 33 | 34 | class ExceptionA2(ExceptionA): 35 | """My testing exception A2.""" 36 | pass 37 | 38 | 39 | class ExceptionB(Exception): 40 | """My testing exception B.""" 41 | pass 42 | 43 | 44 | class ExceptionC(Exception): 45 | """My testing exception C.""" 46 | pass 47 | 48 | 49 | class CustomRule(ErrorRule): 50 | """My custom rule for subclasses.""" 51 | 52 | def match_type(self, exception_type): 53 | return issubclass(exception_type, self._exception_type) 54 | 55 | 56 | class DBusErrorTestCase(unittest.TestCase): 57 | """Test the DBus error register and handler.""" 58 | 59 | def setUp(self): 60 | self.error_mapper = ErrorMapper() 61 | 62 | def _check_type(self, error_name, expected_type): 63 | exception_type = self.error_mapper.get_exception_type(error_name) 64 | self.assertEqual(exception_type, expected_type) 65 | 66 | def _check_name(self, exception_type, expected_name): 67 | error_name = self.error_mapper.get_error_name(exception_type) 68 | self.assertEqual(error_name, expected_name) 69 | 70 | def test_decorators(self): 71 | """Test the error decorators.""" 72 | dbus_error = get_error_decorator(self.error_mapper) 73 | 74 | @dbus_error("org.test.ErrorA") 75 | class DecoratedA(Exception): 76 | pass 77 | 78 | @dbus_error("ErrorB", namespace=("org", "test")) 79 | class DecoratedB(Exception): 80 | pass 81 | 82 | self._check_name(DecoratedA, "org.test.ErrorA") 83 | self._check_type("org.test.ErrorA", DecoratedA) 84 | 85 | self._check_name(DecoratedB, "org.test.ErrorB") 86 | self._check_type("org.test.ErrorB", DecoratedB) 87 | 88 | def test_simple_rule(self): 89 | """Test a simple rule.""" 90 | self.error_mapper.add_rule(ErrorRule( 91 | exception_type=ExceptionA, 92 | error_name="org.test.ErrorA" 93 | )) 94 | 95 | self._check_name(ExceptionA, "org.test.ErrorA") 96 | self._check_name(ExceptionA1, "not.known.Error.ExceptionA1") 97 | self._check_name(ExceptionA2, "not.known.Error.ExceptionA2") 98 | 99 | self._check_type("org.test.ErrorA", ExceptionA) 100 | self._check_type("org.test.ErrorA1", DBusError) 101 | self._check_type("org.test.ErrorA2", DBusError) 102 | 103 | self._check_name(ExceptionB, "not.known.Error.ExceptionB") 104 | self._check_type("org.test.ErrorB", DBusError) 105 | 106 | def test_custom_rule(self): 107 | """Test a custom rule.""" 108 | self.error_mapper.add_rule(CustomRule( 109 | exception_type=ExceptionA, 110 | error_name="org.test.ErrorA" 111 | )) 112 | 113 | self._check_name(ExceptionA, "org.test.ErrorA") 114 | self._check_name(ExceptionA1, "org.test.ErrorA") 115 | self._check_name(ExceptionA2, "org.test.ErrorA") 116 | 117 | self._check_type("org.test.ErrorA", ExceptionA) 118 | self._check_type("org.test.ErrorA1", DBusError) 119 | self._check_type("org.test.ErrorA2", DBusError) 120 | 121 | self._check_name(ExceptionB, "not.known.Error.ExceptionB") 122 | self._check_type("org.test.ErrorB", DBusError) 123 | 124 | def test_several_rules(self): 125 | """Test several rules.""" 126 | self.error_mapper.add_rule(ErrorRule( 127 | exception_type=ExceptionA, 128 | error_name="org.test.ErrorA" 129 | )) 130 | self.error_mapper.add_rule(ErrorRule( 131 | exception_type=ExceptionB, 132 | error_name="org.test.ErrorB" 133 | )) 134 | 135 | self._check_name(ExceptionA, "org.test.ErrorA") 136 | self._check_name(ExceptionB, "org.test.ErrorB") 137 | self._check_name(ExceptionC, "not.known.Error.ExceptionC") 138 | 139 | self._check_type("org.test.ErrorA", ExceptionA) 140 | self._check_type("org.test.ErrorB", ExceptionB) 141 | self._check_type("org.test.ErrorC", DBusError) 142 | 143 | def test_rule_priorities(self): 144 | """Test the priorities of the rules.""" 145 | self.error_mapper.add_rule(ErrorRule( 146 | exception_type=ExceptionA, 147 | error_name="org.test.ErrorA1" 148 | )) 149 | 150 | self._check_name(ExceptionA, "org.test.ErrorA1") 151 | self._check_type("org.test.ErrorA1", ExceptionA) 152 | self._check_type("org.test.ErrorA2", DBusError) 153 | 154 | self.error_mapper.add_rule(ErrorRule( 155 | exception_type=ExceptionA, 156 | error_name="org.test.ErrorA2" 157 | )) 158 | 159 | self._check_name(ExceptionA, "org.test.ErrorA2") 160 | self._check_type("org.test.ErrorA1", ExceptionA) 161 | self._check_type("org.test.ErrorA2", ExceptionA) 162 | 163 | def test_default_mapping(self): 164 | """Test the default error mapping.""" 165 | self._check_name(ExceptionA, "not.known.Error.ExceptionA") 166 | self._check_type("org.test.ErrorB", DBusError) 167 | self._check_type("org.test.ErrorC", DBusError) 168 | 169 | def test_default_class(self): 170 | """Test the default class.""" 171 | self._check_type("org.test.ErrorA", DBusError) 172 | 173 | def test_default_namespace(self): 174 | """Test the default namespace.""" 175 | self._check_name(ExceptionA, "not.known.Error.ExceptionA") 176 | 177 | def test_failed_mapping(self): 178 | """Test the failed mapping.""" 179 | self.error_mapper._error_rules = [] 180 | 181 | with self.assertRaises(LookupError) as cm: 182 | self.error_mapper.get_error_name(ExceptionA) 183 | 184 | self.assertEqual( 185 | "No name found for 'ExceptionA'.", 186 | str(cm.exception) 187 | ) 188 | 189 | with self.assertRaises(LookupError) as cm: 190 | self.error_mapper.get_exception_type("org.test.ErrorA") 191 | 192 | self.assertEqual( 193 | "No type found for 'org.test.ErrorA'.", 194 | str(cm.exception) 195 | ) 196 | -------------------------------------------------------------------------------- /tests/test_identifier.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2019 Red Hat, Inc. All rights reserved. 3 | # 4 | # This library is free software; you can redistribute it and/or 5 | # modify it under the terms of the GNU Lesser General Public 6 | # License as published by the Free Software Foundation; either 7 | # version 2.1 of the License, or (at your option) any later version. 8 | # 9 | # This library is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | # Lesser General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU Lesser General Public 15 | # License along with this library; if not, write to the Free Software 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 17 | # USA 18 | # 19 | import unittest 20 | from unittest.mock import Mock 21 | 22 | from dasbus.identifier import DBusInterfaceIdentifier, DBusObjectIdentifier, \ 23 | DBusServiceIdentifier, DBusBaseIdentifier 24 | 25 | 26 | class DBusIdentifierTestCase(unittest.TestCase): 27 | """Test DBus identifiers.""" 28 | 29 | def assert_namespace(self, obj, namespace): 30 | """Check the DBus namespace object.""" 31 | self.assertEqual(obj.namespace, namespace) 32 | 33 | def assert_interface(self, obj, interface_name): 34 | """Check the DBus interface object.""" 35 | self.assertEqual(obj.interface_name, interface_name) 36 | 37 | def test_identifier(self): 38 | """Test the DBus identifier object.""" 39 | identifier = DBusBaseIdentifier( 40 | namespace=("a", "b", "c") 41 | ) 42 | self.assert_namespace(identifier, ("a", "b", "c")) 43 | 44 | identifier = DBusBaseIdentifier( 45 | basename="d", 46 | namespace=("a", "b", "c") 47 | ) 48 | self.assert_namespace(identifier, ("a", "b", "c", "d")) 49 | 50 | def test_interface(self): 51 | """Test the DBus interface object.""" 52 | interface = DBusInterfaceIdentifier( 53 | namespace=("a", "b", "c") 54 | ) 55 | self.assert_namespace(interface, ("a", "b", "c")) 56 | self.assert_interface(interface, "a.b.c") 57 | 58 | interface = DBusInterfaceIdentifier( 59 | namespace=("a", "b", "c"), 60 | interface_version=1 61 | ) 62 | self.assert_namespace(interface, ("a", "b", "c")) 63 | self.assert_interface(interface, "a.b.c1") 64 | 65 | interface = DBusInterfaceIdentifier( 66 | basename="d", 67 | namespace=("a", "b", "c"), 68 | interface_version=1 69 | ) 70 | self.assert_namespace(interface, ("a", "b", "c", "d")) 71 | self.assert_interface(interface, "a.b.c.d1") 72 | 73 | def assert_object(self, obj, object_path): 74 | """Check the DBus object.""" 75 | self.assertEqual(obj.object_path, object_path) 76 | 77 | def test_object(self): 78 | """Test the DBus object.""" 79 | obj = DBusObjectIdentifier( 80 | namespace=("a", "b", "c") 81 | ) 82 | self.assert_namespace(obj, ("a", "b", "c")) 83 | self.assert_interface(obj, "a.b.c") 84 | self.assert_object(obj, "/a/b/c") 85 | 86 | obj = DBusObjectIdentifier( 87 | namespace=("a", "b", "c"), 88 | object_version=2, 89 | interface_version=4 90 | ) 91 | self.assert_namespace(obj, ("a", "b", "c")) 92 | self.assert_interface(obj, "a.b.c4") 93 | self.assert_object(obj, "/a/b/c2") 94 | 95 | obj = DBusObjectIdentifier( 96 | basename="d", 97 | namespace=("a", "b", "c"), 98 | object_version=2, 99 | interface_version=4 100 | ) 101 | self.assert_namespace(obj, ("a", "b", "c", "d")) 102 | self.assert_interface(obj, "a.b.c.d4") 103 | self.assert_object(obj, "/a/b/c/d2") 104 | 105 | def assert_bus(self, obj, message_bus): 106 | """Check the DBus service object.""" 107 | self.assertEqual(obj.message_bus, message_bus) 108 | 109 | def assert_service(self, obj, service_name): 110 | """Check the DBus service object.""" 111 | self.assertEqual(obj.service_name, service_name) 112 | 113 | def test_service(self): 114 | """Test the DBus service object.""" 115 | bus = Mock() 116 | service = DBusServiceIdentifier( 117 | namespace=("a", "b", "c"), 118 | message_bus=bus 119 | ) 120 | self.assert_namespace(service, ("a", "b", "c")) 121 | self.assert_interface(service, "a.b.c") 122 | self.assert_object(service, "/a/b/c") 123 | self.assert_service(service, "a.b.c") 124 | self.assert_bus(service, bus) 125 | 126 | service = DBusServiceIdentifier( 127 | namespace=("a", "b", "c"), 128 | service_version=3, 129 | interface_version=5, 130 | object_version=7, 131 | message_bus=bus 132 | ) 133 | self.assert_namespace(service, ("a", "b", "c")) 134 | self.assert_interface(service, "a.b.c5") 135 | self.assert_object(service, "/a/b/c7") 136 | self.assert_service(service, "a.b.c3") 137 | self.assert_bus(service, bus) 138 | 139 | service = DBusServiceIdentifier( 140 | basename="d", 141 | namespace=("a", "b", "c"), 142 | service_version=3, 143 | interface_version=5, 144 | object_version=7, 145 | message_bus=bus 146 | ) 147 | self.assert_namespace(service, ("a", "b", "c", "d")) 148 | self.assert_interface(service, "a.b.c.d5") 149 | self.assert_object(service, "/a/b/c/d7") 150 | self.assert_service(service, "a.b.c.d3") 151 | self.assert_bus(service, bus) 152 | 153 | 154 | class DBusServiceIdentifierTestCase(unittest.TestCase): 155 | """Test DBus service identifiers.""" 156 | 157 | def test_get_proxy(self): 158 | """Test getting a proxy.""" 159 | bus = Mock() 160 | namespace = ("a", "b", "c") 161 | 162 | service = DBusServiceIdentifier( 163 | namespace=namespace, 164 | message_bus=bus 165 | ) 166 | 167 | obj = DBusObjectIdentifier( 168 | basename="object", 169 | namespace=namespace 170 | ) 171 | 172 | service.get_proxy() 173 | bus.get_proxy.assert_called_with( 174 | "a.b.c", 175 | "/a/b/c", 176 | None 177 | ) 178 | bus.reset_mock() 179 | 180 | service.get_proxy("/a/b/c/object") 181 | bus.get_proxy.assert_called_with( 182 | "a.b.c", 183 | "/a/b/c/object", 184 | None 185 | ) 186 | bus.reset_mock() 187 | 188 | service.get_proxy(obj) 189 | bus.get_proxy.assert_called_with( 190 | "a.b.c", 191 | "/a/b/c/object", 192 | None 193 | ) 194 | bus.reset_mock() 195 | 196 | def test_get_proxy_for_interface(self): 197 | """Test getting a proxy for an interface.""" 198 | bus = Mock() 199 | namespace = ("a", "b", "c") 200 | 201 | service = DBusServiceIdentifier( 202 | namespace=namespace, 203 | message_bus=bus 204 | ) 205 | 206 | interface = DBusInterfaceIdentifier( 207 | basename="interface", 208 | namespace=namespace 209 | ) 210 | 211 | service.get_proxy( 212 | interface_name="a.b.c.interface" 213 | ) 214 | bus.get_proxy.assert_called_with( 215 | "a.b.c", 216 | "/a/b/c", 217 | "a.b.c.interface" 218 | ) 219 | bus.reset_mock() 220 | 221 | service.get_proxy( 222 | interface_name=interface 223 | ) 224 | bus.get_proxy.assert_called_with( 225 | "a.b.c", 226 | "/a/b/c", 227 | "a.b.c.interface" 228 | ) 229 | bus.reset_mock() 230 | 231 | def test_get_proxy_with_bus_arguments(self): 232 | """Test getting a proxy with an additional arguments.""" 233 | bus = Mock() 234 | error_mapper = Mock() 235 | namespace = ("a", "b", "c") 236 | 237 | service = DBusServiceIdentifier( 238 | namespace=namespace, 239 | message_bus=bus 240 | ) 241 | 242 | service.get_proxy( 243 | error_mapper=error_mapper 244 | ) 245 | bus.get_proxy.assert_called_with( 246 | "a.b.c", 247 | "/a/b/c", 248 | None, 249 | error_mapper=error_mapper 250 | ) 251 | bus.reset_mock() 252 | 253 | service.get_proxy( 254 | interface_name=service, 255 | error_mapper=error_mapper 256 | ) 257 | bus.get_proxy.assert_called_with( 258 | "a.b.c", 259 | "/a/b/c", 260 | "a.b.c", 261 | error_mapper=error_mapper 262 | ) 263 | bus.reset_mock() 264 | -------------------------------------------------------------------------------- /tests/test_namespace.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2019 Red Hat, Inc. All rights reserved. 3 | # 4 | # This library is free software; you can redistribute it and/or 5 | # modify it under the terms of the GNU Lesser General Public 6 | # License as published by the Free Software Foundation; either 7 | # version 2.1 of the License, or (at your option) any later version. 8 | # 9 | # This library is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | # Lesser General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU Lesser General Public 15 | # License along with this library; if not, write to the Free Software 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 17 | # USA 18 | # 19 | import unittest 20 | from dasbus.namespace import get_dbus_name, get_dbus_path, \ 21 | get_namespace_from_name 22 | 23 | 24 | class DBusNamespaceTestCase(unittest.TestCase): 25 | 26 | def test_dbus_name(self): 27 | """Test DBus path.""" 28 | self.assertEqual(get_dbus_name(), "") 29 | self.assertEqual(get_dbus_name("a"), "a") 30 | self.assertEqual(get_dbus_name("a", "b"), "a.b") 31 | self.assertEqual(get_dbus_name("a", "b", "c"), "a.b.c") 32 | self.assertEqual(get_dbus_name("org", "freedesktop", "DBus"), 33 | "org.freedesktop.DBus") 34 | 35 | def test_dbus_path(self): 36 | """Test DBus path.""" 37 | self.assertEqual(get_dbus_path(), "/") 38 | self.assertEqual(get_dbus_path("a"), "/a") 39 | self.assertEqual(get_dbus_path("a", "b"), "/a/b") 40 | self.assertEqual(get_dbus_path("a", "b", "c"), "/a/b/c") 41 | self.assertEqual(get_dbus_path("org", "freedesktop", "DBus"), 42 | "/org/freedesktop/DBus") 43 | 44 | def test_namespace(self): 45 | """Test namespaces.""" 46 | self.assertEqual(get_namespace_from_name("a"), ("a",)) 47 | self.assertEqual(get_namespace_from_name("a.b"), ("a", "b")) 48 | self.assertEqual(get_namespace_from_name("a.b.c"), ("a", "b", "c")) 49 | self.assertEqual(get_namespace_from_name("org.freedesktop.DBus"), 50 | ("org", "freedesktop", "DBus")) 51 | -------------------------------------------------------------------------------- /tests/test_observer.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2019 Red Hat, Inc. All rights reserved. 3 | # 4 | # This library is free software; you can redistribute it and/or 5 | # modify it under the terms of the GNU Lesser General Public 6 | # License as published by the Free Software Foundation; either 7 | # version 2.1 of the License, or (at your option) any later version. 8 | # 9 | # This library is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | # Lesser General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU Lesser General Public 15 | # License along with this library; if not, write to the Free Software 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 17 | # USA 18 | # 19 | import unittest 20 | from unittest.mock import patch, Mock 21 | 22 | from dasbus.constants import DBUS_FLAG_NONE 23 | from dasbus.client.observer import DBusObserver 24 | 25 | 26 | class DBusObserverTestCase(unittest.TestCase): 27 | """Test DBus observers.""" 28 | 29 | def _setup_observer(self, observer): 30 | """Set up the observer.""" 31 | observer._service_available = Mock() 32 | observer._service_unavailable = Mock() 33 | self.assertFalse(observer.is_service_available) 34 | 35 | def _make_service_available(self, observer): 36 | """Make the service available.""" 37 | observer._service_name_appeared_callback() 38 | self._test_if_service_available(observer) 39 | 40 | def _test_if_service_available(self, observer): 41 | """Test if service is available.""" 42 | self.assertTrue(observer.is_service_available) 43 | 44 | observer._service_available.emit.assert_called_once_with(observer) 45 | observer._service_available.reset_mock() 46 | 47 | observer._service_unavailable.emit.assert_not_called() 48 | observer._service_unavailable.reset_mock() 49 | 50 | def _make_service_unavailable(self, observer): 51 | """Make the service unavailable.""" 52 | observer._service_name_vanished_callback() 53 | self._test_if_service_unavailable(observer) 54 | 55 | def _test_if_service_unavailable(self, observer): 56 | """Test if service is unavailable.""" 57 | self.assertFalse(observer.is_service_available) 58 | 59 | observer._service_unavailable.emit.assert_called_once_with(observer) 60 | observer._service_unavailable.reset_mock() 61 | 62 | observer._service_available.emit.assert_not_called() 63 | observer._service_available.reset_mock() 64 | 65 | def test_observer(self): 66 | """Test the observer.""" 67 | observer = DBusObserver(Mock(), "SERVICE") 68 | self._setup_observer(observer) 69 | self._make_service_available(observer) 70 | self._make_service_unavailable(observer) 71 | 72 | @patch("dasbus.client.observer.Gio") 73 | def test_connect(self, gio): 74 | """Test Gio support for watching names.""" 75 | dbus = Mock() 76 | observer = DBusObserver(dbus, "my.service") 77 | self._setup_observer(observer) 78 | 79 | # Connect the observer. 80 | observer.connect_once_available() 81 | 82 | # Check the call. 83 | gio.bus_watch_name_on_connection.assert_called_once() 84 | args, kwargs = gio.bus_watch_name_on_connection.call_args 85 | 86 | self.assertEqual(len(args), 5) 87 | self.assertEqual(len(kwargs), 0) 88 | self.assertEqual(args[0], dbus.connection) 89 | self.assertEqual(args[1], "my.service") 90 | self.assertEqual(args[2], DBUS_FLAG_NONE) 91 | 92 | name_appeared_closure = args[3] 93 | self.assertTrue(callable(name_appeared_closure)) 94 | 95 | name_vanished_closure = args[4] 96 | self.assertTrue(callable(name_vanished_closure)) 97 | 98 | # Check the subscription. 99 | subscription_id = gio.bus_watch_name_on_connection.return_value 100 | self.assertEqual(len(observer._subscriptions), 1) 101 | 102 | # Check the observer. 103 | self.assertFalse(observer.is_service_available) 104 | observer._service_available.emit.assert_not_called() 105 | observer._service_unavailable.emit.assert_not_called() 106 | 107 | # Call the name appeared closure. 108 | name_appeared_closure(dbus.connection, "my.service", "name.owner") 109 | self._test_if_service_available(observer) 110 | 111 | # Call the name vanished closure. 112 | name_vanished_closure(dbus.connection, "my.service") 113 | self._test_if_service_unavailable(observer) 114 | 115 | # Call the name appeared closure again. 116 | name_appeared_closure(dbus.connection, "my.service", "name.owner") 117 | self._test_if_service_available(observer) 118 | 119 | # Disconnect the observer. 120 | observer.disconnect() 121 | 122 | gio.bus_unwatch_name.assert_called_once_with( 123 | subscription_id 124 | ) 125 | 126 | self._test_if_service_unavailable(observer) 127 | self.assertEqual(observer._subscriptions, []) 128 | -------------------------------------------------------------------------------- /tests/test_proxy.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2019 Red Hat, Inc. All rights reserved. 3 | # 4 | # This library is free software; you can redistribute it and/or 5 | # modify it under the terms of the GNU Lesser General Public 6 | # License as published by the Free Software Foundation; either 7 | # version 2.1 of the License, or (at your option) any later version. 8 | # 9 | # This library is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | # Lesser General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU Lesser General Public 15 | # License along with this library; if not, write to the Free Software 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 17 | # USA 18 | # 19 | import unittest 20 | from unittest.mock import Mock 21 | 22 | from dasbus.client.proxy import ObjectProxy, get_object_path 23 | 24 | 25 | class DBusProxyTestCase(unittest.TestCase): 26 | """Test support for object proxies.""" 27 | 28 | def test_get_object_path(self): 29 | """Test get_object_path.""" 30 | proxy = ObjectProxy(Mock(), "my.service", "/my/path") 31 | self.assertEqual(get_object_path(proxy), "/my/path") 32 | 33 | with self.assertRaises(TypeError) as cm: 34 | get_object_path(None) 35 | 36 | self.assertEqual( 37 | "Invalid type 'NoneType'.", 38 | str(cm.exception) 39 | ) 40 | -------------------------------------------------------------------------------- /tests/test_signal.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2019 Red Hat, Inc. All rights reserved. 3 | # 4 | # This library is free software; you can redistribute it and/or 5 | # modify it under the terms of the GNU Lesser General Public 6 | # License as published by the Free Software Foundation; either 7 | # version 2.1 of the License, or (at your option) any later version. 8 | # 9 | # This library is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | # Lesser General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU Lesser General Public 15 | # License along with this library; if not, write to the Free Software 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 17 | # USA 18 | # 19 | import unittest 20 | from unittest.mock import Mock 21 | 22 | from dasbus.server.interface import dbus_signal 23 | from dasbus.signal import Signal 24 | 25 | 26 | class DBusSignalTestCase(unittest.TestCase): 27 | """Test DBus signals.""" 28 | 29 | def test_create_signal(self): 30 | """Create a signal.""" 31 | class Interface(object): 32 | 33 | @dbus_signal 34 | def Signal(self): 35 | pass 36 | 37 | interface = Interface() 38 | signal = interface.Signal 39 | self.assertIsInstance(signal, Signal) 40 | self.assertTrue(hasattr(interface, "__dbus_signal_signal")) 41 | self.assertEqual(getattr(interface, "__dbus_signal_signal"), signal) 42 | 43 | def test_emit_signal(self): 44 | """Emit a signal.""" 45 | class Interface(object): 46 | 47 | @dbus_signal 48 | def Signal(self, a, b, c): 49 | pass 50 | 51 | interface = Interface() 52 | signal = interface.Signal 53 | 54 | callback = Mock() 55 | signal.connect(callback) # pylint: disable=no-member 56 | 57 | signal.emit(1, 2, 3) # pylint: disable=no-member 58 | callback.assert_called_once_with(1, 2, 3) 59 | callback.reset_mock() 60 | 61 | signal.emit(4, 5, 6) # pylint: disable=no-member 62 | callback.assert_called_once_with(4, 5, 6) 63 | callback.reset_mock() 64 | 65 | def test_disconnect_signal(self): 66 | """Disconnect a signal.""" 67 | class Interface(object): 68 | 69 | @dbus_signal 70 | def Signal(self): 71 | pass 72 | 73 | interface = Interface() 74 | callback = Mock() 75 | interface.Signal.connect(callback) # pylint: disable=no-member 76 | 77 | interface.Signal() 78 | callback.assert_called_once_with() 79 | callback.reset_mock() 80 | 81 | interface.Signal.disconnect(callback) # pylint: disable=no-member 82 | interface.Signal() 83 | callback.assert_not_called() 84 | 85 | interface.Signal.connect(callback) # pylint: disable=no-member 86 | interface.Signal.disconnect() # pylint: disable=no-member 87 | interface.Signal() 88 | callback.assert_not_called() 89 | 90 | def test_signals(self): 91 | """Test a class with two signals.""" 92 | class Interface(object): 93 | 94 | @dbus_signal 95 | def Signal1(self): 96 | pass 97 | 98 | @dbus_signal 99 | def Signal2(self): 100 | pass 101 | 102 | interface = Interface() 103 | signal1 = interface.Signal1 104 | signal2 = interface.Signal2 105 | 106 | self.assertNotEqual(signal1, signal2) 107 | 108 | callback1 = Mock() 109 | signal1.connect(callback1) # pylint: disable=no-member 110 | 111 | callback2 = Mock() 112 | signal2.connect(callback2) # pylint: disable=no-member 113 | 114 | signal1.emit() # pylint: disable=no-member 115 | callback1.assert_called_once_with() 116 | callback2.assert_not_called() 117 | callback1.reset_mock() 118 | 119 | signal2.emit() # pylint: disable=no-member 120 | callback1.assert_not_called() 121 | callback2.assert_called_once_with() 122 | 123 | def test_instances(self): 124 | """Test two instances of the class with a signal.""" 125 | class Interface(object): 126 | 127 | @dbus_signal 128 | def Signal(self): 129 | pass 130 | 131 | interface1 = Interface() 132 | signal1 = interface1.Signal 133 | 134 | interface2 = Interface() 135 | signal2 = interface2.Signal 136 | self.assertNotEqual(signal1, signal2) 137 | 138 | callback = Mock() 139 | signal1.connect(callback) # pylint: disable=no-member 140 | 141 | callback2 = Mock() 142 | signal2.connect(callback2) # pylint: disable=no-member 143 | 144 | signal1.emit() # pylint: disable=no-member 145 | callback.assert_called_once_with() 146 | callback2.assert_not_called() 147 | -------------------------------------------------------------------------------- /tests/test_xml.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2019 Red Hat, Inc. All rights reserved. 3 | # 4 | # This library is free software; you can redistribute it and/or 5 | # modify it under the terms of the GNU Lesser General Public 6 | # License as published by the Free Software Foundation; either 7 | # version 2.1 of the License, or (at your option) any later version. 8 | # 9 | # This library is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | # Lesser General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU Lesser General Public 15 | # License along with this library; if not, write to the Free Software 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 17 | # USA 18 | # 19 | import unittest 20 | from dasbus.xml import XMLParser, XMLGenerator 21 | 22 | 23 | class XMLParserTestCase(unittest.TestCase): 24 | 25 | def test_is_member(self): 26 | """Test if the element is a member of an interface.""" 27 | element = XMLParser.xml_to_element('') 28 | self.assertEqual(XMLParser.is_member(element), True) 29 | 30 | element = XMLParser.xml_to_element('') 31 | self.assertEqual(XMLParser.is_member(element), True) 32 | 33 | element = XMLParser.xml_to_element( 34 | '' 38 | ) 39 | self.assertEqual(XMLParser.is_member(element), True) 40 | 41 | def test_is_interface(self): 42 | """Test if the element is an interface.""" 43 | element = XMLParser.xml_to_element( 44 | '' 45 | ) 46 | self.assertEqual(XMLParser.is_interface(element), True) 47 | 48 | def test_is_signal(self): 49 | """Test if the element is a signal.""" 50 | element = XMLParser.xml_to_element('') 51 | self.assertEqual(XMLParser.is_signal(element), True) 52 | 53 | def test_is_method(self): 54 | """Test if the element is a method.""" 55 | element = XMLParser.xml_to_element('') 56 | self.assertEqual(XMLParser.is_method(element), True) 57 | 58 | def test_is_property(self): 59 | """Test if the element is a property.""" 60 | element = XMLParser.xml_to_element( 61 | '' 65 | ) 66 | self.assertEqual(XMLParser.is_property(element), True) 67 | 68 | def test_is_parameter(self): 69 | """Test if the element is a parameter.""" 70 | element = XMLParser.xml_to_element( 71 | '' 75 | ) 76 | self.assertEqual(XMLParser.is_parameter(element), True) 77 | 78 | def test_has_name(self): 79 | """Test if the element has the specified name.""" 80 | element = XMLParser.xml_to_element('') 81 | self.assertEqual(XMLParser.has_name(element, "MethodName"), True) 82 | self.assertEqual(XMLParser.has_name(element, "AnotherName"), False) 83 | 84 | def test_get_name(self): 85 | """Get the name attribute.""" 86 | element = XMLParser.xml_to_element('') 87 | self.assertEqual(XMLParser.get_name(element), "MethodName") 88 | 89 | def test_get_type(self): 90 | """Get the type attribute.""" 91 | element = XMLParser.xml_to_element( 92 | '' 96 | ) 97 | self.assertEqual( 98 | XMLParser.get_type(element), 99 | "ParameterType" 100 | ) 101 | 102 | def test_get_access(self): 103 | """Get the access attribute.""" 104 | element = XMLParser.xml_to_element( 105 | '' 109 | ) 110 | self.assertEqual( 111 | XMLParser.get_access(element), 112 | "PropertyAccess" 113 | ) 114 | 115 | def test_get_direction(self): 116 | """Get the direction attribute.""" 117 | element = XMLParser.xml_to_element( 118 | '' 122 | ) 123 | self.assertEqual( 124 | XMLParser.get_direction(element), 125 | "ParameterDirection" 126 | ) 127 | 128 | def test_get_interfaces_from_node(self): 129 | """Get interfaces from the node.""" 130 | element = XMLParser.xml_to_element(''' 131 | 132 | 133 | 134 | 135 | 136 | ''') 137 | interfaces = XMLParser.get_interfaces_from_node(element) 138 | self.assertEqual(interfaces.keys(), {"A", "B", "C"}) 139 | 140 | 141 | class XMLGeneratorTestCase(unittest.TestCase): 142 | 143 | def _compare(self, element, xml): 144 | self.assertEqual( 145 | XMLGenerator.prettify_xml( 146 | XMLGenerator.element_to_xml(element) 147 | ), 148 | XMLGenerator.prettify_xml(xml) 149 | ) 150 | 151 | def test_node(self): 152 | """Test the node element.""" 153 | self._compare(XMLGenerator.create_node(), '') 154 | 155 | def test_interface(self): 156 | """Test the interface element.""" 157 | self._compare( 158 | XMLGenerator.create_interface("InterfaceName"), 159 | '' 160 | ) 161 | 162 | def test_parameter(self): 163 | """Test the parameter element.""" 164 | self._compare( 165 | XMLGenerator.create_parameter( 166 | "ParameterName", 167 | "ParameterType", 168 | "ParameterDirection" 169 | ), 170 | '' 174 | ) 175 | 176 | def test_property(self): 177 | """Test the property element.""" 178 | self._compare( 179 | XMLGenerator.create_property( 180 | "PropertyName", 181 | "PropertyType", 182 | "PropertyAccess" 183 | ), 184 | '' 188 | ) 189 | 190 | def test_method(self): 191 | """Test the method element.""" 192 | element = XMLGenerator.create_method("MethodName") 193 | xml = '' 194 | self._compare(element, xml) 195 | 196 | def test_signal(self): 197 | """Test the signal element.""" 198 | element = XMLGenerator.create_signal("SignalName") 199 | xml = '' 200 | self._compare(element, xml) 201 | --------------------------------------------------------------------------------