├── .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 | [](https://travis-ci.com/rhinstaller/dasbus)
11 | [](https://dasbus.readthedocs.io/en/latest/?badge=latest)
12 | [](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 |
--------------------------------------------------------------------------------