pax_global_header00006660000000000000000000000064143322013720014507gustar00rootroot0000000000000052 comment=413cf9a0b7c231468f1d28d9a29dd8dd8bda15c5 dasbus-1.7/000077500000000000000000000000001433220137200126375ustar00rootroot00000000000000dasbus-1.7/.codecov.yml000066400000000000000000000005631433220137200150660ustar00rootroot00000000000000codecov: # Don't wait for all other statuses to pass. require_ci_to_pass: false coverage: status: project: # Show a strict status for the code. default: paths: - "dasbus/" patch: # Show an informational status for the patch. default: informational: true # Disable the pull request comment. comment: false dasbus-1.7/.coveragerc000066400000000000000000000003551433220137200147630ustar00rootroot00000000000000[run] source = dasbus/ tests/ concurrency = multiprocessing thread branch = true parallel = true [report] exclude_lines = pragma: no cover raise AssertionError raise NotImplementedError @abstractmethod dasbus-1.7/.gitignore000066400000000000000000000022631433220137200146320ustar00rootroot00000000000000# Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover .hypothesis/ .pytest_cache/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # Jupyter Notebook .ipynb_checkpoints # pyenv .python-version # celery beat schedule file celerybeat-schedule # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ dasbus-1.7/.packit.yaml000066400000000000000000000013741433220137200150610ustar00rootroot00000000000000specfile_path: python-dasbus.spec downstream_package_name: python-dasbus actions: get-current-version: - "python3 ./setup.py --version" create-archive: - "make BUILD_ARGS=sdist archive" - 'bash -c "cp dist/*.tar.gz ."' - 'bash -c "ls *.tar.gz"' jobs: - job: copr_build trigger: pull_request metadata: targets: - fedora-all - job: copr_build trigger: commit metadata: targets: - fedora-rawhide branch: master owner: "@rhinstaller" project: Anaconda preserve_project: True - job: copr_build trigger: commit metadata: targets: - fedora-latest branch: master owner: "@rhinstaller" project: Anaconda-devel preserve_project: True dasbus-1.7/.pylintrc000066400000000000000000000367201433220137200145140ustar00rootroot00000000000000[MASTER] # A comma-separated list of package or module names from where C extensions may # be loaded. Extensions are loading into the active Python interpreter and may # run arbitrary code. extension-pkg-whitelist= # Add files or directories to the blacklist. They should be base names, not # paths. ignore=CVS # Add files or directories matching the regex patterns to the blacklist. The # regex matches against base names, not paths. ignore-patterns= # Python code to execute, usually for sys.path manipulation such as # pygtk.require(). #init-hook= # Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the # number of processors available to use. jobs=1 # Control the amount of potential inferred values when inferring a single # object. This can help the performance when dealing with large functions or # complex, nested conditions. limit-inference-results=100 # List of plugins (as comma separated values of python modules names) to load, # usually to register additional checkers. load-plugins= # Pickle collected data for later comparisons. persistent=yes # Specify a configuration file. #rcfile= # When enabled, pylint would attempt to guess common misconfiguration and emit # user-friendly hints instead of false-positive error messages. suggestion-mode=yes # Allow loading of arbitrary C extensions. Extensions are imported into the # active Python interpreter and may run arbitrary code. unsafe-load-any-extension=no [MESSAGES CONTROL] # Only show warnings with the listed confidence levels. Leave empty to show # all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED. confidence= # Disable the message, report, category or checker with the given id(s). You # can either give multiple identifiers separated by comma (,) or put this # option multiple times (only on the command line, not in the configuration # file where it should appear only once). You can also use "--disable=all" to # disable everything first and then reenable specific checks. For example, if # you want to run only the similarities checker, you can use "--disable=all # --enable=similarities". If you want to run only the classes checker, but have # no Warning level messages displayed, use "--disable=all --enable=classes # --disable=W". disable=invalid-name, missing-docstring, unidiomatic-typecheck, wrong-import-order, ungrouped-imports, wrong-import-position, assigning-non-slot, no-member, no-self-use, bad-option-value, useless-object-inheritance, duplicate-code, too-many-instance-attributes, too-few-public-methods, too-many-public-methods, too-many-arguments, no-else-return, unnecessary-pass, protected-access, arguments-differ, unused-argument, consider-using-f-string # Enable the message, report, category or checker with the given id(s). You can # either give multiple identifier separated by comma (,) or put this option # multiple time (only on the command line, not in the configuration file where # it should appear only once). See also the "--disable" option for examples. enable= [REPORTS] # Python expression which should return a note less than 10 (10 is the highest # note). You have access to the variables errors warning, statement which # respectively contain the number of errors / warnings messages and the total # number of statements analyzed. This is used by the global evaluation report # (RP0004). evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) # Template used to display messages. This is a python new-style format string # used to format the message information. See doc for all details. #msg-template= # Set the output format. Available formats are text, parseable, colorized, json # and msvs (visual studio). You can also give a reporter class, e.g. # mypackage.mymodule.MyReporterClass. output-format=colorized # Tells whether to display a full report or only the messages. reports=no # Activate the evaluation score. score=yes [REFACTORING] # Maximum number of nested blocks for function / method body max-nested-blocks=5 # Complete name of functions that never returns. When checking for # inconsistent-return-statements if a never returning function is called then # it will be considered as an explicit return statement and no message will be # printed. never-returning-functions=sys.exit [FORMAT] # Expected format of line ending, e.g. empty (any line ending), LF or CRLF. expected-line-ending-format= # Regexp for a line that is allowed to be longer than the limit. ignore-long-lines=^\s*(# )??$ # Number of spaces of indent required inside a hanging or continued line. indent-after-paren=4 # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 # tab). indent-string=' ' # Maximum number of characters on a single line. max-line-length=79 # Maximum number of lines in a module. max-module-lines=1000 # Allow the body of a class to be on the same line as the declaration if body # contains single statement. single-line-class-stmt=no # Allow the body of an if to be on the same line as the test if there is no # else. single-line-if-stmt=no [VARIABLES] # List of additional names supposed to be defined in builtins. Remember that # you should avoid defining new builtins when possible. additional-builtins= # Tells whether unused global variables should be treated as a violation. allow-global-unused-variables=yes # List of strings which can identify a callback function by name. A callback # name must start or end with one of those strings. callbacks=cb_, _cb # A regular expression matching the name of dummy variables (i.e. expected to # not be used). dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ # Argument names that match this expression will be ignored. Default to name # with leading underscore. ignored-argument-names=_.*|^ignored_|^unused_ # Tells whether we should check for unused import in __init__ files. init-import=no # List of qualified module names which can have objects that can redefine # builtins. redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io [STRING] # This flag controls whether the implicit-str-concat-in-sequence should # generate a warning on implicit string concatenation in sequences defined over # several lines. check-str-concat-over-line-jumps=no [BASIC] # Naming style matching correct argument names. argument-naming-style=snake_case # Regular expression matching correct argument names. Overrides argument- # naming-style. #argument-rgx= # Naming style matching correct attribute names. attr-naming-style=snake_case # Regular expression matching correct attribute names. Overrides attr-naming- # style. #attr-rgx= # Bad variable names which should always be refused, separated by a comma. bad-names=foo, bar, baz, toto, tutu, tata # Naming style matching correct class attribute names. class-attribute-naming-style=any # Regular expression matching correct class attribute names. Overrides class- # attribute-naming-style. #class-attribute-rgx= # Naming style matching correct class names. class-naming-style=PascalCase # Regular expression matching correct class names. Overrides class-naming- # style. #class-rgx= # Naming style matching correct constant names. const-naming-style=UPPER_CASE # Regular expression matching correct constant names. Overrides const-naming- # style. #const-rgx= # Minimum line length for functions/classes that require docstrings, shorter # ones are exempt. docstring-min-length=-1 # Naming style matching correct function names. function-naming-style=snake_case # Regular expression matching correct function names. Overrides function- # naming-style. #function-rgx= # Good variable names which should always be accepted, separated by a comma. good-names=i, j, k, ex, Run, _ # Include a hint for the correct naming format with invalid-name. include-naming-hint=no # Naming style matching correct inline iteration names. inlinevar-naming-style=any # Regular expression matching correct inline iteration names. Overrides # inlinevar-naming-style. #inlinevar-rgx= # Naming style matching correct method names. method-naming-style=snake_case # Regular expression matching correct method names. Overrides method-naming- # style. #method-rgx= # Naming style matching correct module names. module-naming-style=snake_case # Regular expression matching correct module names. Overrides module-naming- # style. #module-rgx= # Colon-delimited sets of names that determine each other's naming style when # the name regexes allow several styles. name-group= # Regular expression which should only match function or class names that do # not require a docstring. no-docstring-rgx=^_ # List of decorators that produce properties, such as abc.abstractproperty. Add # to this list to register other decorators that produce valid properties. # These decorators are taken in consideration only for invalid-name. property-classes=abc.abstractproperty # Naming style matching correct variable names. variable-naming-style=snake_case # Regular expression matching correct variable names. Overrides variable- # naming-style. #variable-rgx= [MISCELLANEOUS] # List of note tags to take in consideration, separated by a comma. notes=FIXME, XXX, TODO [TYPECHECK] # List of decorators that produce context managers, such as # contextlib.contextmanager. Add to this list to register other decorators that # produce valid context managers. contextmanager-decorators=contextlib.contextmanager # List of members which are set dynamically and missed by pylint inference # system, and so shouldn't trigger E1101 when accessed. Python regular # expressions are accepted. generated-members= # Tells whether missing members accessed in mixin class should be ignored. A # mixin class is detected if its name ends with "mixin" (case insensitive). ignore-mixin-members=yes # Tells whether to warn about missing members when the owner of the attribute # is inferred to be None. ignore-none=yes # This flag controls whether pylint should warn about no-member and similar # checks whenever an opaque object is returned when inferring. The inference # can return multiple potential results while evaluating a Python object, but # some branches might not be evaluated, which results in partial inference. In # that case, it might be useful to still emit no-member and other checks for # the rest of the inferred objects. ignore-on-opaque-inference=yes # List of class names for which member attributes should not be checked (useful # for classes with dynamically set attributes). This supports the use of # qualified names. ignored-classes=ObjectProxy, InterfaceProxy # List of module names for which member attributes should not be checked # (useful for modules/projects where namespaces are manipulated during runtime # and thus existing member attributes cannot be deduced by static analysis. It # supports qualified module names, as well as Unix pattern matching. ignored-modules= # Show a hint with possible names when a member name was not found. The aspect # of finding the hint is based on edit distance. missing-member-hint=yes # The minimum edit distance a name should have in order to be considered a # similar match for a missing member name. missing-member-hint-distance=1 # The total number of similar names that should be taken in consideration when # showing a hint for a missing member. missing-member-max-choices=1 [SPELLING] # Limits count of emitted suggestions for spelling mistakes. max-spelling-suggestions=4 # Spelling dictionary name. Available dictionaries: en_NG (myspell), en_MW # (myspell), en_TT (myspell), en_GB (myspell), en_AG (myspell), en_BS # (myspell), en_DK (myspell), en_AU (myspell), en_ZA (myspell), en_ZW # (myspell), en_IE (myspell), en_SG (myspell), en_HK (myspell), en_BZ # (myspell), en_NZ (myspell), en_BW (myspell), en_NA (myspell), en_ZM # (myspell), en_CA (myspell), en_PH (myspell), en_US (myspell), en_JM # (myspell), en_GH (myspell), en_IN (myspell).. spelling-dict= # List of comma separated words that should not be checked. spelling-ignore-words= # A path to a file that contains private dictionary; one word per line. spelling-private-dict-file= # Tells whether to store unknown words to indicated private dictionary in # --spelling-private-dict-file option instead of raising a message. spelling-store-unknown-words=no [SIMILARITIES] # Ignore comments when computing similarities. ignore-comments=yes # Ignore docstrings when computing similarities. ignore-docstrings=yes # Ignore imports when computing similarities. ignore-imports=no # Minimum lines number of a similarity. min-similarity-lines=4 [LOGGING] # Format style used to check logging format string. `old` means using % # formatting, while `new` is for `{}` formatting. logging-format-style=old # Logging modules to check that the string format arguments are in logging # function parameter format. logging-modules=logging [DESIGN] # Maximum number of arguments for function / method. max-args=5 # Maximum number of attributes for a class (see R0902). max-attributes=7 # Maximum number of boolean expressions in an if statement. max-bool-expr=5 # Maximum number of branch for function / method body. max-branches=12 # Maximum number of locals for function / method body. max-locals=15 # Maximum number of parents for a class (see R0901). max-parents=7 # Maximum number of public methods for a class (see R0904). max-public-methods=20 # Maximum number of return / yield for function / method body. max-returns=6 # Maximum number of statements in function / method body. max-statements=50 # Minimum number of public methods for a class (see R0903). min-public-methods=2 [IMPORTS] # Allow wildcard imports from modules that define __all__. allow-wildcard-with-all=no # Analyse import fallback blocks. This can be used to support both Python 2 and # 3 compatible code, which means that the block might have code that exists # only in one or another interpreter, leading to false positives when analysed. analyse-fallback-blocks=no # Deprecated modules which should not be used, separated by a comma. deprecated-modules=optparse,tkinter.tix,mock # Create a graph of external dependencies in the given file (report RP0402 must # not be disabled). ext-import-graph= # Create a graph of every (i.e. internal and external) dependencies in the # given file (report RP0402 must not be disabled). import-graph= # Create a graph of internal dependencies in the given file (report RP0402 must # not be disabled). int-import-graph= # Force import order to recognize a module as part of the standard # compatibility libraries. known-standard-library= # Force import order to recognize a module as part of a third party library. known-third-party=enchant [CLASSES] # List of method names used to declare (i.e. assign) instance attributes. defining-attr-methods=__init__, __new__, setUp # List of member names, which should be excluded from the protected access # warning. exclude-protected= # List of valid names for the first argument in a class method. valid-classmethod-first-arg=cls # List of valid names for the first argument in a metaclass class method. valid-metaclass-classmethod-first-arg=cls [EXCEPTIONS] # Exceptions that will emit a warning when being caught. Defaults to # "BaseException, Exception". overgeneral-exceptions=BaseException, Exception dasbus-1.7/.readthedocs.yml000066400000000000000000000004421433220137200157250ustar00rootroot00000000000000# Read the Docs configuration file # See: https://docs.readthedocs.io/en/stable/config-file/v2.html # The version of the configuration file. version: 2 # Formats of the documentation to be built. formats: all # Configuration for Conda support. conda: environment: docs/environment.yml dasbus-1.7/.travis.yml000066400000000000000000000023031433220137200147460ustar00rootroot00000000000000language: minimal dist: focal services: docker sudo: required env: - IMAGE=python TAG=3.11-rc - IMAGE=python TAG=3.10 - IMAGE=python TAG=3.9 - IMAGE=python TAG=3.8 - IMAGE=python TAG=3.7 - IMAGE=fedora TAG=rawhide - IMAGE=centos TAG=stream9 - IMAGE=centos TAG=stream8 - IMAGE=debian TAG=latest - IMAGE=ubuntu TAG=devel before_install: # Create a docker image called "myimage". - docker build -t myimage -f ".travis/Dockerfile.${IMAGE}" --build-arg TAG="${TAG}" . install: # Install CI dependencies. pip install --user codecov before_script: # Create a docker container from the image, mount the current working # directory to /app inside the container and let the container running # in the background. The resulting container ID will be saved to # container_id so we can reference it later on. - docker run --volume "$(pwd):/app" --workdir "/app" --tty --detach myimage bash > container_id script: # Run tests in the running container. - docker exec "$(cat container_id)" make ci after_success: # Analyze the code coverage. - docker exec "$(cat container_id)" coverage3 xml -i - codecov after_script: # Stop the container. - docker stop "$(cat container_id)" dasbus-1.7/.travis/000077500000000000000000000000001433220137200142255ustar00rootroot00000000000000dasbus-1.7/.travis/Dockerfile.centos000066400000000000000000000005071433220137200175130ustar00rootroot00000000000000ARG TAG=stream9 FROM quay.io/centos/centos:${TAG} ENV LANG=C.utf8 RUN cat /etc/os-release RUN dnf -y update && \ dnf -y install \ make \ python3 \ python3-pip \ python3-gobject-base \ dbus-daemon \ && dnf clean all RUN pip3 install \ sphinx \ sphinx_rtd_theme \ coverage \ pylint dasbus-1.7/.travis/Dockerfile.debian000066400000000000000000000005001433220137200174330ustar00rootroot00000000000000ARG TAG=latest FROM debian:${TAG} ENV LANG=C.utf8 RUN cat /etc/os-release RUN apt-get update && \ apt-get install -y \ make \ python3 \ python3-pip \ python3-gi \ dbus-daemon \ && rm -rf /var/lib/apt/lists/* RUN pip3 install \ sphinx \ sphinx_rtd_theme \ coverage \ pylint dasbus-1.7/.travis/Dockerfile.fedora000066400000000000000000000005611433220137200174600ustar00rootroot00000000000000ARG TAG=rawhide FROM registry.fedoraproject.org/fedora:${TAG} ENV LANG=C.utf8 RUN cat /etc/os-release RUN dnf -y update && \ dnf -y install \ gcc \ make \ python3 \ python3-pip \ python3-wheel \ python3-gobject-base \ dbus-daemon \ && dnf clean all RUN pip3 install \ sphinx \ sphinx_rtd_theme \ coverage \ pylint dasbus-1.7/.travis/Dockerfile.python000066400000000000000000000005511433220137200175400ustar00rootroot00000000000000ARG TAG=latest FROM python:${TAG} ENV LANG=C.utf8 RUN cat /etc/os-release RUN apt-get update && \ apt-get install -y \ make \ gcc \ pkg-config \ python3-dev \ libgirepository1.0-dev \ dbus-daemon \ && rm -rf /var/lib/apt/lists/* RUN pip3 install \ PyGObject \ sphinx \ sphinx_rtd_theme \ coverage \ pylint dasbus-1.7/.travis/Dockerfile.ubuntu000066400000000000000000000006061433220137200175420ustar00rootroot00000000000000ARG TAG=devel FROM ubuntu:${TAG} ENV LANG=C.utf8 # Disable interaction with tzdata. ENV DEBIAN_FRONTEND=noninteractive RUN cat /etc/os-release RUN apt-get update && \ apt-get install -y \ make \ python3 \ python3-pip \ python3-gi \ dbus-daemon \ && rm -rf /var/lib/apt/lists/* RUN pip3 install \ sphinx \ sphinx_rtd_theme \ coverage \ pylint dasbus-1.7/LICENSE000066400000000000000000000636361433220137200136620ustar00rootroot00000000000000 GNU LESSER GENERAL PUBLIC LICENSE Version 2.1, February 1999 Copyright (C) 1991, 1999 Free Software Foundation, Inc. 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. [This is the first released version of the Lesser GPL. It also counts as the successor of the GNU Library Public License, version 2, hence the version number 2.1.] Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public Licenses are intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This license, the Lesser General Public License, applies to some specially designated software packages--typically libraries--of the Free Software Foundation and other authors who decide to use it. You can use it too, but we suggest you first think carefully about whether this license or the ordinary General Public License is the better strategy to use in any particular case, based on the explanations below. When we speak of free software, we are referring to freedom of use, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish); that you receive source code or can get it if you want it; that you can change the software and use pieces of it in new free programs; and that you are informed that you can do these things. To protect your rights, we need to make restrictions that forbid distributors to deny you these rights or to ask you to surrender these rights. These restrictions translate to certain responsibilities for you if you distribute copies of the library or if you modify it. For example, if you distribute copies of the library, whether gratis or for a fee, you must give the recipients all the rights that we gave you. You must make sure that they, too, receive or can get the source code. If you link other code with the library, you must provide complete object files to the recipients, so that they can relink them with the library after making changes to the library and recompiling it. And you must show them these terms so they know their rights. We protect your rights with a two-step method: (1) we copyright the library, and (2) we offer you this license, which gives you legal permission to copy, distribute and/or modify the library. To protect each distributor, we want to make it very clear that there is no warranty for the free library. Also, if the library is modified by someone else and passed on, the recipients should know that what they have is not the original version, so that the original author's reputation will not be affected by problems that might be introduced by others. Finally, software patents pose a constant threat to the existence of any free program. We wish to make sure that a company cannot effectively restrict the users of a free program by obtaining a restrictive license from a patent holder. Therefore, we insist that any patent license obtained for a version of the library must be consistent with the full freedom of use specified in this license. Most GNU software, including some libraries, is covered by the ordinary GNU General Public License. This license, the GNU Lesser General Public License, applies to certain designated libraries, and is quite different from the ordinary General Public License. We use this license for certain libraries in order to permit linking those libraries into non-free programs. When a program is linked with a library, whether statically or using a shared library, the combination of the two is legally speaking a combined work, a derivative of the original library. The ordinary General Public License therefore permits such linking only if the entire combination fits its criteria of freedom. The Lesser General Public License permits more lax criteria for linking other code with the library. We call this license the "Lesser" General Public License because it does Less to protect the user's freedom than the ordinary General Public License. It also provides other free software developers Less of an advantage over competing non-free programs. These disadvantages are the reason we use the ordinary General Public License for many libraries. However, the Lesser license provides advantages in certain special circumstances. For example, on rare occasions, there may be a special need to encourage the widest possible use of a certain library, so that it becomes a de-facto standard. To achieve this, non-free programs must be allowed to use the library. A more frequent case is that a free library does the same job as widely used non-free libraries. In this case, there is little to gain by limiting the free library to free software only, so we use the Lesser General Public License. In other cases, permission to use a particular library in non-free programs enables a greater number of people to use a large body of free software. For example, permission to use the GNU C Library in non-free programs enables many more people to use the whole GNU operating system, as well as its variant, the GNU/Linux operating system. Although the Lesser General Public License is Less protective of the users' freedom, it does ensure that the user of a program that is linked with the Library has the freedom and the wherewithal to run that program using a modified version of the Library. The precise terms and conditions for copying, distribution and modification follow. Pay close attention to the difference between a "work based on the library" and a "work that uses the library". The former contains code derived from the library, whereas the latter must be combined with the library in order to run. GNU LESSER GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License Agreement applies to any software library or other program which contains a notice placed by the copyright holder or other authorized party saying it may be distributed under the terms of this Lesser General Public License (also called "this License"). Each licensee is addressed as "you". A "library" means a collection of software functions and/or data prepared so as to be conveniently linked with application programs (which use some of those functions and data) to form executables. The "Library", below, refers to any such software library or work which has been distributed under these terms. A "work based on the Library" means either the Library or any derivative work under copyright law: that is to say, a work containing the Library or a portion of it, either verbatim or with modifications and/or translated straightforwardly into another language. (Hereinafter, translation is included without limitation in the term "modification".) "Source code" for a work means the preferred form of the work for making modifications to it. For a library, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the library. Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running a program using the Library is not restricted, and output from such a program is covered only if its contents constitute a work based on the Library (independent of the use of the Library in a tool for writing it). Whether that is true depends on what the Library does and what the program that uses the Library does. 1. You may copy and distribute verbatim copies of the Library's complete source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and distribute a copy of this License along with the Library. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Library or any portion of it, thus forming a work based on the Library, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) The modified work must itself be a software library. b) You must cause the files modified to carry prominent notices stating that you changed the files and the date of any change. c) You must cause the whole of the work to be licensed at no charge to all third parties under the terms of this License. d) If a facility in the modified Library refers to a function or a table of data to be supplied by an application program that uses the facility, other than as an argument passed when the facility is invoked, then you must make a good faith effort to ensure that, in the event an application does not supply such function or table, the facility still operates, and performs whatever part of its purpose remains meaningful. (For example, a function in a library to compute square roots has a purpose that is entirely well-defined independent of the application. Therefore, Subsection 2d requires that any application-supplied function or table used by this function must be optional: if the application does not supply it, the square root function must still compute square roots.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Library, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Library, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Library. In addition, mere aggregation of another work not based on the Library with the Library (or with a work based on the Library) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may opt to apply the terms of the ordinary GNU General Public License instead of this License to a given copy of the Library. To do this, you must alter all the notices that refer to this License, so that they refer to the ordinary GNU General Public License, version 2, instead of to this License. (If a newer version than version 2 of the ordinary GNU General Public License has appeared, then you can specify that version instead if you wish.) Do not make any other change in these notices. Once this change is made in a given copy, it is irreversible for that copy, so the ordinary GNU General Public License applies to all subsequent copies and derivative works made from that copy. This option is useful when you wish to copy part of the code of the Library into a program that is not a library. 4. You may copy and distribute the Library (or a portion or derivative of it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange. If distribution of object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place satisfies the requirement to distribute the source code, even though third parties are not compelled to copy the source along with the object code. 5. A program that contains no derivative of any portion of the Library, but is designed to work with the Library by being compiled or linked with it, is called a "work that uses the Library". Such a work, in isolation, is not a derivative work of the Library, and therefore falls outside the scope of this License. However, linking a "work that uses the Library" with the Library creates an executable that is a derivative of the Library (because it contains portions of the Library), rather than a "work that uses the library". The executable is therefore covered by this License. Section 6 states terms for distribution of such executables. When a "work that uses the Library" uses material from a header file that is part of the Library, the object code for the work may be a derivative work of the Library even though the source code is not. Whether this is true is especially significant if the work can be linked without the Library, or if the work is itself a library. The threshold for this to be true is not precisely defined by law. If such an object file uses only numerical parameters, data structure layouts and accessors, and small macros and small inline functions (ten lines or less in length), then the use of the object file is unrestricted, regardless of whether it is legally a derivative work. (Executables containing this object code plus portions of the Library will still fall under Section 6.) Otherwise, if the work is a derivative of the Library, you may distribute the object code for the work under the terms of Section 6. Any executables containing that work also fall under Section 6, whether or not they are linked directly with the Library itself. 6. As an exception to the Sections above, you may also combine or link a "work that uses the Library" with the Library to produce a work containing portions of the Library, and distribute that work under terms of your choice, provided that the terms permit modification of the work for the customer's own use and reverse engineering for debugging such modifications. You must give prominent notice with each copy of the work that the Library is used in it and that the Library and its use are covered by this License. You must supply a copy of this License. If the work during execution displays copyright notices, you must include the copyright notice for the Library among them, as well as a reference directing the user to the copy of this License. Also, you must do one of these things: a) Accompany the work with the complete corresponding machine-readable source code for the Library including whatever changes were used in the work (which must be distributed under Sections 1 and 2 above); and, if the work is an executable linked with the Library, with the complete machine-readable "work that uses the Library", as object code and/or source code, so that the user can modify the Library and then relink to produce a modified executable containing the modified Library. (It is understood that the user who changes the contents of definitions files in the Library will not necessarily be able to recompile the application to use the modified definitions.) b) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (1) uses at run time a copy of the library already present on the user's computer system, rather than copying library functions into the executable, and (2) will operate properly with a modified version of the library, if the user installs one, as long as the modified version is interface-compatible with the version that the work was made with. c) Accompany the work with a written offer, valid for at least three years, to give the same user the materials specified in Subsection 6a, above, for a charge no more than the cost of performing this distribution. d) If distribution of the work is made by offering access to copy from a designated place, offer equivalent access to copy the above specified materials from the same place. e) Verify that the user has already received a copy of these materials or that you have already sent this user a copy. For an executable, the required form of the "work that uses the Library" must include any data and utility programs needed for reproducing the executable from it. However, as a special exception, the materials to be distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. It may happen that this requirement contradicts the license restrictions of other proprietary libraries that do not normally accompany the operating system. Such a contradiction means you cannot use both them and the Library together in an executable that you distribute. 7. You may place library facilities that are a work based on the Library side-by-side in a single library together with other library facilities not covered by this License, and distribute such a combined library, provided that the separate distribution of the work based on the Library and of the other library facilities is otherwise permitted, and provided that you do these two things: a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities. This must be distributed under the terms of the Sections above. b) Give prominent notice with the combined library of the fact that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. 8. You may not copy, modify, sublicense, link with, or distribute the Library except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense, link with, or distribute the Library is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 9. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Library or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Library (or any work based on the Library), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Library or works based on it. 10. Each time you redistribute the Library (or any work based on the Library), the recipient automatically receives a license from the original licensor to copy, distribute, link with or modify the Library subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties with this License. 11. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Library at all. For example, if a patent license would not permit royalty-free redistribution of the Library by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Library. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply, and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 12. If the distribution and/or use of the Library is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Library under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 13. The Free Software Foundation may publish revised and/or new versions of the Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Library specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Library does not specify a license version number, you may choose any version ever published by the Free Software Foundation. 14. If you wish to incorporate parts of the Library into other free programs whose distribution conditions are incompatible with these, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Libraries If you develop a new library, and you want it to be of the greatest possible use to the public, we recommend making it free software that everyone can redistribute and change. You can do so by permitting redistribution under these terms (or, alternatively, under the terms of the ordinary General Public License). To apply these terms, attach the following notices to the library. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Also add information on how to contact you by electronic and paper mail. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the library, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the library `Frob' (a library for tweaking knobs) written by James Random Hacker. , 1 April 1990 Ty Coon, President of Vice That's all there is to it! dasbus-1.7/MANIFEST.in000066400000000000000000000002701433220137200143740ustar00rootroot00000000000000# Include the README include *.md # Include the license file include LICENSE # Include the tests recursive-include tests *.py # Include the examples recursive-include examples *.py dasbus-1.7/Makefile000066400000000000000000000064711433220137200143070ustar00rootroot00000000000000# Copyright (C) 2019 Red Hat, Inc. All rights reserved. # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 # USA # PKGNAME = dasbus VERSION = $(shell awk '/Version:/ { print $$2 }' python-$(PKGNAME).spec) TAG = v$(VERSION) PYTHON ?= python3 COVERAGE ?= coverage3 # Container-related CI_NAME = $(PKGNAME)-ci CI_IMAGE ?= fedora CI_TAG ?= latest CI_CMD ?= make ci # Arguments used for setup.py call for creating archive BUILD_ARGS ?= sdist bdist_wheel # Arguments used by pylint for checking the code. CHECK_ARGS ?= .PHONY: clean clean: git clean -idx .PHONY: container-ci container-ci: podman build \ --file ".travis/Dockerfile.$(CI_IMAGE)" \ --build-arg TAG=$(CI_TAG) \ --tag $(CI_NAME) \ --pull-always podman run \ --volume .:/dasbus:Z \ --workdir /dasbus \ $(CI_NAME) $(CI_CMD) .PHONY: ci ci: @echo "*** Running CI with $(PYTHON) ***" $(PYTHON) --version $(MAKE) check $(MAKE) test $(MAKE) docs .PHONY: check check: @echo "*** Running pylint ***" $(PYTHON) -m pylint --version $(PYTHON) -m pylint $(CHECK_ARGS) dasbus/ tests/ .PHONY: test test: @echo "*** Running unittests with $(COVERAGE) ***" PYTHONPATH=. $(COVERAGE) run --rcfile=.coveragerc -m unittest discover -v -s tests/ $(COVERAGE) combine $(COVERAGE) report -m --include="dasbus/*" | tee coverage-report.log .PHONY: docs docs: $(MAKE) -C docs html text .PHONY: changelog changelog: @git log --no-merges --pretty="format:- %s (%ae)" $(TAG).. | sed -e 's/@.*)/)/' .PHONY: commit commit: @NEWSUBVER=$$((`echo $(VERSION) | cut -d . -f 2` + 1)) ; \ NEWVERSION=`echo $(VERSION).$$NEWSUBVER | cut -d . -f 1,3` ; \ DATELINE="* `LC_ALL=C.UTF-8 date "+%a %b %d %Y"` `git config user.name` <`git config user.email`> - $$NEWVERSION-1" ; \ cl=`grep -n %changelog python-${PKGNAME}.spec | cut -d : -f 1` ; \ tail --lines=+$$(($$cl + 1)) python-${PKGNAME}.spec > speclog ; \ (head -n $$cl python-${PKGNAME}.spec ; echo "$$DATELINE" ; make --quiet changelog 2>/dev/null ; echo ""; cat speclog) > python-${PKGNAME}.spec.new ; \ mv python-${PKGNAME}.spec.new python-${PKGNAME}.spec ; rm -f speclog ; \ sed -i "s/Version:\( *\)$(VERSION)/Version:\1$$NEWVERSION/" python-${PKGNAME}.spec ; \ sed -i "s/version=\"$(VERSION)\"/version=\"$$NEWVERSION\"/" setup.py ; \ git add python-${PKGNAME}.spec setup.py ; \ git commit -m "New release: $$NEWVERSION" .PHONY: tag tag: git tag -a -m "Tag as $(VERSION)" -f $(TAG) @echo "Tagged as $(TAG)" .PHONY: push push: @echo "Run the command 'git push --follow-tags' with '--dry-run' first." .PHONY: archive archive: $(PYTHON) setup.py ${BUILD_ARGS} @echo "The archive is in dist/$(PKGNAME)-$(VERSION).tar.gz" .PHONY: upload upload: $(PYTHON) -m twine upload dist/* dasbus-1.7/README.md000066400000000000000000000130601433220137200141160ustar00rootroot00000000000000# dasbus This DBus library is written in Python 3, based on GLib and inspired by pydbus. Find out more in the [documentation](https://dasbus.readthedocs.io/en/latest/). The code used to be part of the [Anaconda Installer](https://github.com/rhinstaller/anaconda) project. It was based on the [pydbus](https://github.com/LEW21/pydbus) library, but we replaced it with our own solution because its upstream development stalled. The dasbus library is a result of this effort. [![Build Status](https://travis-ci.com/rhinstaller/dasbus.svg?branch=master)](https://travis-ci.com/rhinstaller/dasbus) [![Documentation Status](https://readthedocs.org/projects/dasbus/badge/?version=latest)](https://dasbus.readthedocs.io/en/latest/?badge=latest) [![codecov](https://codecov.io/gh/rhinstaller/dasbus/branch/master/graph/badge.svg)](https://codecov.io/gh/rhinstaller/dasbus) ## Requirements * Python 3.6+ * PyGObject 3 You can install [PyGObject](https://pygobject.readthedocs.io) provided by your operating system or use PyPI. The system package is usually called `python3-gi`, `python3-gobject` or `pygobject3`. See the [instructions](https://pygobject.readthedocs.io/en/latest/getting_started.html) for your platform (only for PyGObject, you don't need cairo or GTK). The library is known to work with Python 3.8, PyGObject 3.34 and GLib 2.63, but these are not the required minimal versions. ## Installation Install the package from [PyPI](https://pypi.org/project/dasbus/) or install the package provided by your operating system if available. ### Install from PyPI Follow the instructions above to install the requirements before you install `dasbus` with `pip`. The required dependencies has to be installed manually in this case. ``` pip3 install dasbus ``` ### Install the system package Follow the instructions for your operating system to install the `python-dasbus` package. The required dependencies should be installed automatically by the system package manager. * [Arch Linux](https://dasbus.readthedocs.io/en/latest/#install-on-arch-linux) * [Debian / Ubuntu](https://dasbus.readthedocs.io/en/latest/#install-on-debian-ubuntu) * [Fedora / CentOS / RHEL](https://dasbus.readthedocs.io/en/latest/#install-on-fedora-centos-rhel) * [openSUSE](https://dasbus.readthedocs.io/en/latest/#install-on-opensuse) ## Examples Show the current hostname. ```python from dasbus.connection import SystemMessageBus bus = SystemMessageBus() proxy = bus.get_proxy( "org.freedesktop.hostname1", "/org/freedesktop/hostname1" ) print(proxy.Hostname) ``` Send a notification to the notification server. ```python from dasbus.connection import SessionMessageBus bus = SessionMessageBus() proxy = bus.get_proxy( "org.freedesktop.Notifications", "/org/freedesktop/Notifications" ) id = proxy.Notify( "", 0, "face-smile", "Hello World!", "This notification can be ignored.", [], {}, 0 ) print("The notification {} was sent.".format(id)) ``` Handle a closed notification. ```python from dasbus.loop import EventLoop loop = EventLoop() from dasbus.connection import SessionMessageBus bus = SessionMessageBus() proxy = bus.get_proxy( "org.freedesktop.Notifications", "/org/freedesktop/Notifications" ) def callback(id, reason): print("The notification {} was closed.".format(id)) proxy.NotificationClosed.connect(callback) loop.run() ``` Asynchronously fetch a list of network devices. ```python from dasbus.loop import EventLoop loop = EventLoop() from dasbus.connection import SystemMessageBus bus = SystemMessageBus() proxy = bus.get_proxy( "org.freedesktop.NetworkManager", "/org/freedesktop/NetworkManager" ) def callback(call): print(call()) proxy.GetDevices(callback=callback) loop.run() ``` Inhibit the system suspend and hibernation. ```python import os from dasbus.connection import SystemMessageBus from dasbus.unix import GLibClientUnix bus = SystemMessageBus() proxy = bus.get_proxy( "org.freedesktop.login1", "/org/freedesktop/login1", client=GLibClientUnix ) fd = proxy.Inhibit( "sleep", "my-example", "Running an example", "block" ) proxy.ListInhibitors() os.close(fd) ``` Define the org.example.HelloWorld service. ```python class HelloWorld(object): __dbus_xml__ = """ """ def Hello(self, name): return "Hello {}!".format(name) ``` Define the org.example.HelloWorld service with an automatically generated XML specification. ```python from dasbus.server.interface import dbus_interface from dasbus.typing import Str @dbus_interface("org.example.HelloWorld") class HelloWorld(object): def Hello(self, name: Str) -> Str: return "Hello {}!".format(name) print(HelloWorld.__dbus_xml__) ``` Publish the org.example.HelloWorld service on the session message bus. ```python from dasbus.connection import SessionMessageBus bus = SessionMessageBus() bus.publish_object("/org/example/HelloWorld", HelloWorld()) bus.register_service("org.example.HelloWorld") from dasbus.loop import EventLoop loop = EventLoop() loop.run() ``` See more examples in the [documentation](https://dasbus.readthedocs.io/en/latest/examples.html). ## Inspiration Look at the [complete examples](https://github.com/rhinstaller/dasbus/tree/master/examples) or [DBus services](https://github.com/rhinstaller/anaconda/tree/master/pyanaconda/modules) of the Anaconda Installer for more inspiration. dasbus-1.7/dasbus/000077500000000000000000000000001433220137200141205ustar00rootroot00000000000000dasbus-1.7/dasbus/__init__.py000066400000000000000000000000001433220137200162170ustar00rootroot00000000000000dasbus-1.7/dasbus/client/000077500000000000000000000000001433220137200153765ustar00rootroot00000000000000dasbus-1.7/dasbus/client/__init__.py000066400000000000000000000000001433220137200174750ustar00rootroot00000000000000dasbus-1.7/dasbus/client/handler.py000066400000000000000000000403601433220137200173700ustar00rootroot00000000000000# # Client support for DBus objects # # Copyright (C) 2019 Red Hat, Inc. All rights reserved. # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 # USA # from abc import ABCMeta, abstractmethod from functools import partial from dasbus.client.property import PropertyProxy from dasbus.error import ErrorMapper from dasbus.signal import Signal from dasbus.constants import DBUS_FLAG_NONE from dasbus.specification import DBusSpecification from dasbus.typing import get_variant, get_variant_type, unwrap_variant import gi gi.require_version("Gio", "2.0") gi.require_version("GLib", "2.0") from gi.repository import Gio, GLib __all__ = [ "GLibClient", "AbstractClientObjectHandler", "ClientObjectHandler" ] class GLibClient(object): """The low-level DBus client library based on GLib.""" # Infinite timeout of a DBus call DBUS_TIMEOUT_NONE = GLib.MAXINT @classmethod def sync_call(cls, connection, service_name, object_path, interface_name, method_name, parameters, reply_type, flags=DBUS_FLAG_NONE, timeout=DBUS_TIMEOUT_NONE): """Synchronously call a DBus method. :return: a result of the DBus call """ return connection.call_sync( service_name, object_path, interface_name, method_name, parameters, reply_type, flags, timeout, None ) @classmethod def async_call(cls, connection, service_name, object_path, interface_name, method_name, parameters, reply_type, callback, callback_args=(), flags=DBUS_FLAG_NONE, timeout=DBUS_TIMEOUT_NONE): """Asynchronously call a DBus method.""" connection.call( service_name, object_path, interface_name, method_name, parameters, reply_type, flags, timeout, callback=cls._async_call_finish, user_data=(callback, callback_args) ) @classmethod def _async_call_finish(cls, source_object, result_object, user_data): """Finish an asynchronous DBus method call.""" # Prepare the user's callback. callback, callback_args = user_data # Call user's callback. callback( lambda: source_object.call_finish(result_object), *callback_args ) @classmethod def subscribe_signal(cls, connection, service_name, object_path, interface_name, signal_name, callback, callback_args=(), flags=DBUS_FLAG_NONE): """Subscribe to a signal. :return: a callback to unsubscribe """ subscription_id = connection.signal_subscribe( service_name, interface_name, signal_name, object_path, None, flags, callback=cls._signal_callback, user_data=(callback, callback_args) ) return partial( cls._unsubscribe_signal, connection, subscription_id ) @classmethod def _signal_callback(cls, connection, sender_name, object_path, interface_name, signal_name, parameters, user_data): """A callback that is called when a DBus signal is emitted.""" # Prepare the user's callback. callback, callback_args = user_data # Call user's callback. callback(parameters, *callback_args) @classmethod def _unsubscribe_signal(cls, connection, subscription_id): """Unsubscribe from a signal.""" connection.signal_unsubscribe(subscription_id) @classmethod def is_timeout_error(cls, error): """Is it a timeout error?""" return isinstance(error, GLib.Error) \ and error.matches(Gio.io_error_quark(), Gio.IOErrorEnum.TIMED_OUT) @classmethod def is_remote_error(cls, error): """Is it a remote DBus error?""" return isinstance(error, GLib.Error) \ and Gio.DBusError.is_remote_error(error) @classmethod def get_remote_error_name(cls, error): """Get a DBus name of the remote DBus error.""" return Gio.DBusError.get_remote_error(error) @classmethod def get_remote_error_message(cls, error): """Get a message of the remote DBus error.""" name = cls.get_remote_error_name(error) message = error.message prefix = "{}:{}: ".format("GDBus.Error", name) if message.startswith(prefix): return message[len(prefix):] return message class AbstractClientObjectHandler(metaclass=ABCMeta): """The abstract handler of a remote DBus object.""" __slots__ = [ "_message_bus", "_service_name", "_object_path", "_specification" ] def __init__(self, message_bus, service_name, object_path): """Create a new handler. :param message_bus: a message bus :param service_name: a DBus name of the service :param object_path: a DBus path the object """ self._message_bus = message_bus self._service_name = service_name self._object_path = object_path self._specification = None @property def service_name(self): """DBus service name. :return: a DBus name """ return self._service_name @property def object_path(self): """DBus object path. :return: a DBus path """ return self._object_path @property def specification(self): """DBus specification.""" if not self._specification: self._specification = self._get_specification() return self._specification @abstractmethod def _get_specification(self): """Introspect the DBus object. :return: a DBus specification """ return DBusSpecification() def create_member(self, interface_name, member_name): """Create a member of the DBus object. :param interface_name: a name of the interface :param member_name: a name of the member :return: a signal, a method or a property """ spec = self._find_member_spec(interface_name, member_name) handler = self._find_handler(type(spec)) return handler(spec) def _find_member_spec(self, interface_name, member_name): """Find a specification of the DBus member. :param interface_name: a name of the interface :param member_name: a name of the member :return: a specification of the member """ return self.specification.get_member( interface_name, member_name ) def _find_handler(self, member_type): """Find a handler for the given member type. :param member_type: a type of the member :return: a callback """ if member_type is DBusSpecification.Property: return self._get_property if member_type is DBusSpecification.Method: return self._get_method if member_type is DBusSpecification.Signal: return self._get_signal raise TypeError( "Unsupported type: {}".format(member_type.__name__) ) @abstractmethod def _get_property(self, property_spec): """Get a proxy of the DBus property. :param property_spec: a property_specification :return: a property object """ pass @abstractmethod def _get_method(self, method_spec): """Get a proxy of the DBus method. :param method_spec: a method specification :return: a callable object """ pass @abstractmethod def _get_signal(self, signal_spec): """Get a proxy of the DBus signal. :param signal_spec: a signal specification :return: a signal object """ pass @abstractmethod def disconnect_members(self): """Disconnect members of the DBus object. Unsubscribe from DBus signals and disconnect all registered callbacks of the proxy signals. """ pass class ClientObjectHandler(AbstractClientObjectHandler): """The client handler of a DBus object.""" __slots__ = [ "_client", "_signal_factory", "_error_mapper", "_subscriptions" ] def __init__(self, message_bus, service_name, object_path, error_mapper=None, client=GLibClient, signal_factory=Signal): """Create a new handler. :param message_bus: a message bus :param service_name: a DBus name of the service :param object_path: a DBus path the object :param error_mapper: a DBus error mapper :param client: a DBus client library :param signal_factory: a signal factory """ super().__init__(message_bus, service_name, object_path) self._client = client self._signal_factory = signal_factory self._error_mapper = error_mapper or ErrorMapper() self._subscriptions = [] def _get_specification(self): """Introspect the DBus object.""" xml = self._call_method( "org.freedesktop.DBus.Introspectable", "Introspect", None, "(s)" ) return DBusSpecification.from_xml(xml) def _get_signal(self, signal_spec): """Get a proxy of the DBus signal.""" # Create a signal. signal = self._signal_factory() # Subscribe to a DBus signal. unsubscribe = self._client.subscribe_signal( self._message_bus.connection, self._service_name, self._object_path, signal_spec.interface_name, signal_spec.name, callback=self._signal_callback, callback_args=(signal.emit,) ) # Keep the subscriptions. self._subscriptions.append(unsubscribe) self._subscriptions.append(signal.disconnect) return signal def _signal_callback(self, parameters, callback): """A callback that is called when a DBus signal is emitted.""" callback(*unwrap_variant(parameters)) def _get_property(self, property_spec): """Get a proxy of the DBus property.""" getter = None setter = None if property_spec.readable: getter = partial(self._get_property_value, property_spec) if property_spec.writable: setter = partial(self._set_property_value, property_spec) return PropertyProxy(getter, setter) def _get_property_value(self, property_spec): """Get a value of the DBus property.""" variant = self._call_method( "org.freedesktop.DBus.Properties", "Get", "(ss)", "(v)", property_spec.interface_name, property_spec.name ) return unwrap_variant(variant) def _set_property_value(self, property_spec, property_value): """Set a value of the DBus property.""" return self._call_method( "org.freedesktop.DBus.Properties", "Set", "(ssv)", None, property_spec.interface_name, property_spec.name, get_variant(property_spec.type, property_value) ) def _get_method(self, method_spec): """Get a callable proxy of the DBus method.""" return partial( self._call_method, method_spec.interface_name, method_spec.name, method_spec.in_type, method_spec.out_type ) def _call_method(self, interface_name, method_name, in_type, out_type, *parameters, **kwargs): """Call a DBus method. :return: a result of the call or None """ # Create variants. if not parameters: parameters = None if in_type is not None: parameters = get_variant(in_type, parameters) # Create variant types. reply_type = None if out_type is not None: reply_type = get_variant_type(out_type) # Collect arguments. args = ( self._message_bus.connection, self._service_name, self._object_path, interface_name, method_name, parameters, reply_type, ) # Get the callback. callback = kwargs.pop("callback", None) callback_args = kwargs.pop("callback_args", tuple()) # Choose the type of invocation. if not callback: return self._get_method_reply( self._client.sync_call, *args, **kwargs, ) else: return self._client.async_call( *args, **kwargs, callback=self._method_callback, callback_args=(callback, callback_args) ) def _method_callback(self, getter, callback, callback_args): """A callback of an asynchronous DBus method call.""" callback( lambda: self._get_method_reply(getter), *callback_args ) def _get_method_reply(self, call, *args, **kwargs): """Get a result of a DBus call. :param call: a callback :param args: arguments of the callback :param kwargs: keyword arguments of the callback :return: a result of the callback :raise: an exception raised by the callback """ try: result = call(*args, **kwargs) return self._handle_method_result(result) except Exception as error: # pylint: disable=broad-except return self._handle_method_error(error) def _handle_method_error(self, error): """Handle an error of a DBus call. If the call returned a DBus error, it will be mapped to a Python exception based on the rules defined in the available error mapper. It should be a subclass of DBusError. :param error: an exception raised during the call :raise Exception: if the call unexpectedly failed :raise TimeoutError: if the DBus call timed out :raise DBusError: if the call returned a DBus error """ if self._client.is_remote_error(error): # Handle a remote DBus error. name = self._client.get_remote_error_name(error) cls = self._error_mapper.get_exception_type(name) message = self._client.get_remote_error_message(error) # Create a new exception. exception = cls(message) exception.dbus_name = name # Raise a new instance of the exception class. raise exception from None if self._client.is_timeout_error(error): # Handle a timeout error. raise TimeoutError( "The DBus call timeout was reached." ) from None # Or re-raise the original error. raise error def _handle_method_result(self, result): """Handle a result of a DBus call. :param result: a variant tuple """ # Unwrap a variant tuple. values = unwrap_variant(result) # Return None if there are no values. if not values: return None # Return one value. if len(values) == 1: return values[0] # Return multiple values. return values def disconnect_members(self): """Disconnect members of the DBus object.""" while self._subscriptions: callback = self._subscriptions.pop() callback() dasbus-1.7/dasbus/client/observer.py000066400000000000000000000161331433220137200176030ustar00rootroot00000000000000# # Client support for DBus observers # # Copyright (C) 2019 Red Hat, Inc. All rights reserved. # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 # USA # import logging from functools import partial from dasbus.constants import DBUS_FLAG_NONE from dasbus.signal import Signal import gi gi.require_version("Gio", "2.0") from gi.repository import Gio log = logging.getLogger(__name__) __all__ = [ "DBusObserverError", "DBusObserver", "GLibMonitoring" ] class DBusObserverError(Exception): """Exception class for the DBus observers.""" pass class GLibMonitoring(object): """The low-level DBus monitoring library based on GLib.""" @classmethod def watch_name(cls, connection, name, flags=DBUS_FLAG_NONE, name_appeared=None, name_vanished=None): """Watch a service name on the DBus connection.""" name_appeared_closure = None name_vanished_closure = None if name_appeared: name_appeared_closure = partial( cls._name_appeared_callback, user_data=(name_appeared, ()) ) if name_vanished: name_vanished_closure = partial( cls._name_vanished_callback, user_data=(name_vanished, ()) ) registration_id = Gio.bus_watch_name_on_connection( connection, name, flags, name_appeared_closure, name_vanished_closure ) return partial( cls._unwatch_name, connection, registration_id ) @classmethod def _name_appeared_callback(cls, connection, name, name_owner, user_data): """Callback for watch_name..""" # Prepare the user's callback. callback, callback_args = user_data # Call user's callback. callback(name_owner, *callback_args) @classmethod def _name_vanished_callback(cls, connection, name, user_data): """Callback for watch_name.""" # Prepare the user's callback. callback, callback_args = user_data # Call user's callback. callback(*callback_args) @classmethod def _unwatch_name(cls, connection, registration_id): """Stops watching a service name on the DBus connection.""" Gio.bus_unwatch_name(registration_id) class DBusObserver(object): """Base class for DBus observers. This class is recommended to use only to watch the availability of a service on DBus. It doesn't provide any support for accessing objects provided by the service. Usage: .. code-block:: python # Create the observer and connect to its signals. observer = DBusObserver(SystemBus, "org.freedesktop.NetworkManager") def callback1(observer): print("Service is available!") def callback2(observer): print("Service is unavailable!") observer.service_available.connect(callback1) observer.service_unavailable.connect(callback2) # Connect to the service once it is available. observer.connect_once_available() # Disconnect the observer. observer.disconnect() """ def __init__(self, message_bus, service_name, monitoring=GLibMonitoring): """Creates a DBus service observer. :param message_bus: a message bus :param service_name: a DBus name of a service """ self._message_bus = message_bus self._service_name = service_name self._is_service_available = False self._service_available = Signal() self._service_unavailable = Signal() self._monitoring = monitoring self._subscriptions = [] @property def service_name(self): """Returns a DBus name.""" return self._service_name @property def is_service_available(self): """The proxy can be accessed.""" return self._is_service_available @property def service_available(self): """Signal that emits when the service is available. Signal emits this class as an argument. You have to call the watch method to activate the signals. """ return self._service_available @property def service_unavailable(self): """Signal that emits when the service is unavailable. Signal emits this class as an argument. You have to call the watch method to activate the signals. """ return self._service_unavailable def connect_once_available(self): """Connect to the service once it is available. The observer is not connected to the service until it emits the service_available signal. """ self._watch() def disconnect(self): """Disconnect from the service. Disconnect from the service if it is connected and stop watching its availability. """ self._unwatch() if self.is_service_available: self._disable_service() def _watch(self): """Watch the service name on DBus.""" subscription = self._monitoring.watch_name( self._message_bus.connection, self.service_name, DBUS_FLAG_NONE, self._service_name_appeared_callback, self._service_name_vanished_callback ) self._subscriptions.append(subscription) def _unwatch(self): """Stop to watch the service name on DBus.""" while self._subscriptions: callback = self._subscriptions.pop() callback() def _enable_service(self): """Enable the service.""" self._is_service_available = True self._service_available.emit(self) def _disable_service(self): """Disable the service.""" self._is_service_available = False self._service_unavailable.emit(self) def _service_name_appeared_callback(self, *args): """Callback for the watch method.""" if not self.is_service_available: self._enable_service() def _service_name_vanished_callback(self, *args): """Callback for the watch method.""" if self.is_service_available: self._disable_service() def __str__(self): """Returns a string version of this object.""" return self._service_name def __repr__(self): """Returns a string representation.""" return "{}({})".format( self.__class__.__name__, self._service_name ) dasbus-1.7/dasbus/client/property.py000066400000000000000000000036711433220137200176430ustar00rootroot00000000000000# # Client support for DBus properties # # Copyright (C) 2019 Red Hat, Inc. All rights reserved. # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 # USA # __all__ = ["PropertyProxy"] class PropertyProxy(object): """Proxy of a remote DBus property. It can be used to define instance attributes. """ __slots__ = [ "_getter", "_setter" ] def __init__(self, getter, setter): """Create a new proxy of the DBus property.""" self._getter = getter self._setter = setter def get(self): """Get the value of the DBus property.""" return self.__get__(None, None) # pylint: disable=unnecessary-dunder-call def __get__(self, instance, owner): if instance is None and owner: return self if not self._getter: raise AttributeError( "Can't read DBus property." ) return self._getter() def set(self, value): """Set the value of the DBus property.""" return self.__set__(None, value) # pylint: disable=unnecessary-dunder-call def __set__(self, instance, value): if not self._setter: raise AttributeError( "Can't set DBus property." ) return self._setter(value) dasbus-1.7/dasbus/client/proxy.py000066400000000000000000000160431433220137200171350ustar00rootroot00000000000000# # Client support for DBus proxies # # Copyright (C) 2019 Red Hat, Inc. All rights reserved. # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 # USA # from abc import ABCMeta, abstractmethod from threading import Lock from dasbus.client.handler import ClientObjectHandler from dasbus.client.property import PropertyProxy from dasbus.specification import DBusSpecificationError __all__ = [ "AbstractObjectProxy", "ObjectProxy", "InterfaceProxy", "get_object_path", "disconnect_proxy" ] def get_object_handler(proxy): """Get an object handler of the DBus proxy. :param proxy: a DBus proxy :return: a DBus proxy handler """ if not isinstance(proxy, AbstractObjectProxy): raise TypeError("Invalid type '{}'.".format(type(proxy).__name__)) return getattr(proxy, "_handler") def get_object_path(proxy): """Get an object path of the remote DBus object. :param proxy: a DBus proxy :return: a DBus path """ handler = get_object_handler(proxy) return handler.object_path def disconnect_proxy(proxy): """Disconnect the DBus proxy from the remote object. :param proxy: a DBus proxy """ handler = get_object_handler(proxy) handler.disconnect_members() class AbstractObjectProxy(metaclass=ABCMeta): """Abstract proxy of a remote DBus object.""" __slots__ = [ "_handler", "_members", "_lock", "__weakref__" ] # Set of local instance attributes. _locals = {*__slots__} def __init__(self, message_bus, service_name, object_path, handler_factory=ClientObjectHandler, **handler_arguments): """Create a new proxy. :param message_bus: a message bus :param service_name: a DBus name of the service :param object_path: a DBus path the object :param handler_factory: a factory of a DBus client object handler :param handler_arguments: additional arguments for the handler factory """ self._handler = handler_factory( message_bus, service_name, object_path, **handler_arguments ) self._members = {} self._lock = Lock() @abstractmethod def _get_interface(self, member_name): """Get the DBus interface of the member. :param member_name: a member name :return: an interface name """ pass def _get_member(self, *key): """Find a member of the DBus object. If the member doesn't exist, we will acquire a lock and ask a handler to create it. This method is thread-safe. :param key: a member key :return: a member :raise: AttributeError if invalid """ try: return self._members[key] except KeyError: pass return self._create_member(*key) def _create_member(self, *key): """Create a member of the DBus object. If the member doesn't exist, ask a handler to create it. This method is thread-safe. :param key: a member key :return: a member :raise: DBusSpecificationError if invalid """ with self._lock: try: return self._members[key] except KeyError: pass try: member = self._handler.create_member(*key) except DBusSpecificationError as e: raise AttributeError(str(e)) from None self._members[key] = member return member def __getattr__(self, name): """Get the attribute. Called when an attribute lookup has not found the attribute in the usual places. Always call the DBus handler in this case. """ member = self._get_member(self._get_interface(name), name) if isinstance(member, PropertyProxy): return member.get() return member def __setattr__(self, name, value): """Set the attribute. Called when an attribute assignment is attempted. Call the DBus handler if the name is not a name of an instance attribute defined in _locals. """ if name in self._locals: return super().__setattr__(name, value) member = self._get_member(self._get_interface(name), name) if isinstance(member, PropertyProxy): return member.set(value) raise AttributeError( "Can't set DBus attribute '{}'.".format(name) ) class ObjectProxy(AbstractObjectProxy): """Proxy of a remote DBus object.""" __slots__ = ["_interface_names"] # Set of instance attributes. _locals = {*AbstractObjectProxy._locals, *__slots__} def __init__(self, *args, **kwargs): """Create a new proxy. :param handler: a DBus client object handler """ super().__init__(*args, **kwargs) self._interface_names = None def _get_interface(self, member_name): """Get the DBus interface of the member. The members of standard interfaces have a priority. """ if self._interface_names is None: members = reversed( self._handler.specification.members ) self._interface_names = { m.name: m.interface_name for m in members } try: return self._interface_names[member_name] except KeyError: pass raise AttributeError( "DBus object has no attribute '{}'.".format(member_name) ) class InterfaceProxy(AbstractObjectProxy): """Proxy of a remote DBus interface.""" __slots__ = ["_interface_name"] # Set of instance attributes. _locals = {*AbstractObjectProxy._locals, *__slots__} def __init__(self, message_bus, service_name, object_path, interface_name, *args, **kwargs): """Create a new proxy. :param message_bus: a message bus :param service_name: a DBus name of the service :param object_path: a DBus path the object :param handler: a DBus client object handler """ super().__init__(message_bus, service_name, object_path, *args, **kwargs) self._interface_name = interface_name def _get_interface(self, member_name): """Get the DBus interface of the member.""" return self._interface_name dasbus-1.7/dasbus/connection.py000066400000000000000000000265361433220137200166450ustar00rootroot00000000000000# # Representation of DBus connections # # Copyright (C) 2019 Red Hat, Inc. All rights reserved. # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 # USA # import logging import threading from abc import ABCMeta, abstractmethod from dasbus.constants import DBUS_NAME_FLAG_ALLOW_REPLACEMENT, \ DBUS_REQUEST_NAME_REPLY_PRIMARY_OWNER from dasbus.client.proxy import ObjectProxy, InterfaceProxy from dasbus.error import ErrorMapper from dasbus.server.handler import ServerObjectHandler import gi gi.require_version("Gio", "2.0") from gi.repository import Gio log = logging.getLogger(__name__) __all__ = [ "GLibConnection", "MessageBus", "SystemMessageBus", "SessionMessageBus", "AddressedMessageBus" ] class GLibConnection(object): """The low-level DBus connection library based on GLib.""" DEFAULT_FLAGS = ( Gio.DBusConnectionFlags.AUTHENTICATION_CLIENT | Gio.DBusConnectionFlags.MESSAGE_BUS_CONNECTION ) @staticmethod def get_system_bus_connection(cancellable=None): """Get a system bus connection.""" log.info("Connecting to the system bus.") return Gio.bus_get_sync( Gio.BusType.SYSTEM, cancellable ) @staticmethod def get_session_bus_connection(cancellable=None): """Get a session bus connection.""" log.info("Connecting to the session bus.") return Gio.bus_get_sync( Gio.BusType.SESSION, cancellable ) @staticmethod def get_addressed_bus_connection(bus_address, flags=DEFAULT_FLAGS, observer=None, cancellable=None): """Get a connection to a bus at the specified address.""" return Gio.DBusConnection.new_for_address_sync( bus_address, flags, observer, cancellable ) class AbstractMessageBus(metaclass=ABCMeta): """Abstract representation of a message bus. The property connection represents a connection to the bus. You can register a service name with register_service, or publish an object with publish_object and get a proxy of a remote object with get_proxy. """ @property @abstractmethod def connection(self): """The DBus connection.""" return None def check_connection(self): """Check if the connection is set up. :return: True if the connection is set up otherwise False """ try: return self.connection is not None except Exception as e: # pylint: disable=broad-except log.warning("Connection can't be created:\n%s", e) return False @abstractmethod def get_proxy(self, service_name, object_path, interface_name=None, **kwargs): """Returns a proxy of a remote DBus object. :param service_name: a DBus name of a service :param object_path: a DBus path of an object :param interface_name: a DBus name of an interface or None :return: a proxy object """ pass @abstractmethod def register_service(self, service_name, **kwargs): """Register a service on DBus. A service can be registered by requesting its name on DBus. This method should be called only after all of the required objects of the service are published on DBus. :param service_name: a DBus name of a service """ pass @abstractmethod def publish_object(self, object_path, obj, **kwargs): """Publish an object on DBus. :param object_path: a DBus path of an object :param obj: an instance of @dbus_interface or @dbus_class """ pass @abstractmethod def disconnect(self): """Disconnect from DBus.""" pass class MessageBus(AbstractMessageBus): """Representation of a message bus based on D-Bus.""" def __init__(self, error_mapper=None, provider=GLibConnection): """Create a new message bus. :param error_mapper: a DBus error mapper :param provider: a provider of DBus connections """ super().__init__() self._provider = provider self._error_mapper = error_mapper or ErrorMapper() self._connection = None self._proxy = None self._registrations = {} self._requested_names = set() @property def connection(self): """The DBus connection.""" if not self._connection: self._connection = self._get_connection() return self._connection @abstractmethod def _get_connection(self): """Return a DBus connection.""" pass @property def proxy(self): """The proxy of DBus.""" if not self._proxy: self._proxy = self.get_proxy( "org.freedesktop.DBus", "/org/freedesktop/DBus" ) return self._proxy # pylint: disable=arguments-differ def get_proxy(self, service_name, object_path, interface_name=None, proxy_factory=None, **proxy_arguments): """Returns a proxy of a remote DBus object. If the proxy factory is not specified, we will use a default one. If the interface name is set, we will choose InterfaceProxy, otherwise ObjectProxy. If the interface name is set, we will add it to the additional arguments for the proxy factory. :param service_name: a DBus name of a service :param object_path: a DBus path of an object :param interface_name: a DBus name of an interface or None :param proxy_factory: a factory of a DBus object proxy :param proxy_arguments: additional arguments for the proxy factory :return: a proxy object """ self._check_service_access(service_name) if not proxy_factory: proxy_factory = InterfaceProxy if interface_name else ObjectProxy if interface_name: proxy_arguments["interface_name"] = interface_name return proxy_factory( self, service_name, object_path, error_mapper=self._error_mapper, **proxy_arguments ) def _check_service_access(self, service_name): """Check if we can access a DBus service. FIXME: This is a temporary check that should be later removed. This is useful during the transition of the Anaconda code from UI to DBus modules. This check prevents a deadlock in case that a DBus module tries to access a service, that it provides, from the main thread. :param service_name: a DBus name of a service :raises: RuntimeError if the service cannot be accessed """ if service_name not in self._requested_names: # We don't provide this service. return if threading.current_thread() is not threading.main_thread(): # We don't try to access this service from the main thread. return raise RuntimeError( "Can't access DBus service '{}' from " "the main thread.".format(service_name) ) # pylint: disable=arguments-differ def register_service(self, service_name, flags=DBUS_NAME_FLAG_ALLOW_REPLACEMENT): """Register a service on DBus. :param service_name: a DBus name of a service :param flags: the flags argument of the RequestName DBus method """ log.debug("Registering a service name %s.", service_name) result = self.proxy.RequestName(service_name, flags) if result != DBUS_REQUEST_NAME_REPLY_PRIMARY_OWNER: raise ConnectionError("Name request has failed: {}".format(result)) self._requested_names.add( service_name ) self._registrations[service_name] = ( lambda: self.proxy.ReleaseName(service_name) ) # pylint: disable=arguments-differ def publish_object(self, object_path, obj, server_factory=ServerObjectHandler, **server_arguments): """Publish an object on DBus. :param object_path: a DBus path of an object :param obj: an instance of @dbus_interface or @dbus_class :param server_factory: a factory of a DBus server object handler :param server_arguments: additional arguments for the server factory """ log.debug("Publishing an object at %s.", object_path) object_handler = server_factory( self, object_path, obj, error_mapper=self._error_mapper, **server_arguments ) object_handler.connect_object() self._registrations[object_path] = object_handler.disconnect_object def _unregister(self, object_path): """Remove Object from DBus.""" if object_path in self._registrations: callback = self._registrations.pop(object_path) callback() def unpublish_object(self, object_path): log.debug("Removing object %s from the bus.", object_path) self._unregister(object_path) def unregister_service(self, object_path): log.debug("Removing service %s from the bus.", object_path) self._unregister(object_path) def disconnect(self): """Disconnect from DBus.""" log.debug("Disconnecting from the bus.") while self._registrations: _, callback = self._registrations.popitem() callback() self._connection = None self._requested_names = set() class SystemMessageBus(MessageBus): """Representation of a system bus connection.""" def _get_connection(self): """Get a system DBus connection.""" log.info("Connecting to the system bus.") return self._provider.get_system_bus_connection() class SessionMessageBus(MessageBus): """Representation of a session bus connection.""" def _get_connection(self): """Get a session DBus connection.""" log.info("Connecting to the session bus.") return self._provider.get_session_bus_connection() class AddressedMessageBus(MessageBus): """Representation of a connection for the specified address.""" def __init__(self, address, *args, **kwargs): """Create a new representation of a connection. :param address: a bus address """ super().__init__(*args, **kwargs) self._address = address @property def address(self): """The bus address.""" return self._address def _get_connection(self): """Get a connection to a bus at the specified address.""" log.info("Connecting to a bus at %s.", self._address) return self._provider.get_addressed_bus_connection(self._address) dasbus-1.7/dasbus/constants.py000066400000000000000000000025401433220137200165070ustar00rootroot00000000000000# # DBus constants # # Copyright (C) 2019 Red Hat, Inc. All rights reserved. # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 # USA # # Status codes. DBUS_START_REPLY_SUCCESS = 1 # No flags are set. DBUS_FLAG_NONE = 0 # System environment variable holding the DBus session address. DBUS_STARTER_ADDRESS = "DBUS_STARTER_ADDRESS" # Return values of org.freedesktop.DBus.RequestName. DBUS_REQUEST_NAME_REPLY_PRIMARY_OWNER = 1 DBUS_REQUEST_NAME_REPLY_IN_QUEUE = 2 DBUS_REQUEST_NAME_REPLY_EXISTS = 3 DBUS_REQUEST_NAME_REPLY_ALREADY_OWNER = 4 # Flags of org.freedesktop.DBus.RequestName. DBUS_NAME_FLAG_ALLOW_REPLACEMENT = 0x1 DBUS_NAME_FLAG_REPLACE_EXISTING = 0x2 DBUS_NAME_FLAG_DO_NOT_QUEUE = 0x3 dasbus-1.7/dasbus/error.py000066400000000000000000000211351433220137200156250ustar00rootroot00000000000000# # Support for DBus errors # # Copyright (C) 2019 Red Hat, Inc. All rights reserved. # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 # USA # from abc import ABCMeta, abstractmethod from dasbus.namespace import get_dbus_name __all__ = [ "get_error_decorator", "DBusError", "AbstractErrorRule", "ErrorRule", "DefaultErrorRule", "ErrorMapper" ] def get_error_decorator(error_mapper): """Generate a decorator for DBus errors. Create a function for decorating Python exception classes. The decorator will add a new rule to the given error mapper that will map the class to the specified error name. Definition of the decorator: .. code-block:: python decorator(error_name, namespace=()) The decorator accepts a name of the DBus error and optionally a namespace of the DBus name. The namespace will be used as a prefix of the DBus name. Usage: .. code-block:: python # Create an error mapper. error_mapper = ErrorMapper() # Create a decorator for DBus errors and use it to map # the class ExampleError to the name my.example.Error. dbus_error = create_error_decorator(error_mapper) @dbus_error("my.example.Error") class ExampleError(DBusError): pass :param error_mapper: an error mapper :return: a decorator """ def decorator(error_name, namespace=()): error_name = get_dbus_name(*namespace, error_name) def decorated(cls): error_mapper.add_rule(ErrorRule( exception_type=cls, error_name=error_name )) return cls return decorated return decorator class DBusError(Exception): """A default DBus error.""" pass class AbstractErrorRule(metaclass=ABCMeta): """Abstract rule for mapping a Python exception to a DBus error.""" __slots__ = [] @abstractmethod def match_type(self, exception_type): """Is this rule matching the given exception type? :param exception_type: a type of the Python error :return: True or False """ pass @abstractmethod def get_name(self, exception_type): """Get a DBus name for the given exception type. :param exception_type: a type of the Python error :return: a name of the DBus error """ pass @abstractmethod def match_name(self, error_name): """Is this rule matching the given DBus error? :param error_name: a name of the DBus error :return: True or False """ pass @abstractmethod def get_type(self, error_name): """Get an exception type of the given DBus error. param error_name: a name of the DBus error :return: a type of the Python error """ pass class ErrorRule(AbstractErrorRule): """Rule for mapping a Python exception to a DBus error.""" __slots__ = [ "_exception_type", "_error_name" ] def __init__(self, exception_type, error_name): """Create a new error rule. The rule will return the Python type exception_type for the DBue error error_name. The rule will return the DBue name error_name for the Python type exception_type :param exception_type: a type of the Python error :param error_name: a name of the DBus error """ self._exception_type = exception_type self._error_name = error_name def match_type(self, exception_type): """Is this rule matching the given exception type?""" return self._exception_type == exception_type def get_name(self, exception_type): """Get a DBus name for the given exception type.""" return self._error_name def match_name(self, error_name): """Is this rule matching the given DBus error?""" return self._error_name == error_name def get_type(self, error_name): """Get an exception type of the given DBus error.""" return self._exception_type class DefaultErrorRule(AbstractErrorRule): """Default rule for mapping a Python exception to a DBus error.""" __slots__ = [ "_default_type", "_default_namespace" ] def __init__(self, default_type, default_namespace): """Create a new default rule. The rule will return the Python type default_type for the all DBus errors. The rule will generate a DBus name with the prefix default_namespace for all Python exception types. :param default_type: a default type of the Python error :param default_namespace: a default namespace of the DBus error """ self._default_type = default_type self._default_namespace = default_namespace def match_type(self, exception_type): """Is this rule matching the given exception type?""" return True def get_name(self, exception_type): """Get a DBus name for the given exception type.""" return get_dbus_name(*self._default_namespace, exception_type.__name__) def match_name(self, error_name): """Is this rule matching the given DBus error?""" return True def get_type(self, error_name): """Get an exception type of the given DBus error.""" return self._default_type class ErrorMapper(object): """Class for mapping Python exceptions to DBus errors.""" __slots__ = ["_error_rules"] def __init__(self): """Create a new error mapper.""" self._error_rules = [] self.reset_rules() def add_rule(self, rule: AbstractErrorRule): """Add a rule to the error mapper. The new rule will have a higher priority than the rules already contained in the error mapper. :param rule: an error rule :type rule: an instance of AbstractErrorRule """ self._error_rules.append(rule) def reset_rules(self): """Reset rules in the error mapper. Reset the error rules to the initial state. All rules will be replaced with the default ones. """ # Clear the list. self._error_rules = [] # Add the default rules. self.add_rule(DefaultErrorRule( default_type=DBusError, default_namespace=("not", "known", "Error") )) def get_error_name(self, exception_type): """Get a DBus name of the Python exception. Try to find a matching rule in the error mapper. If a rule matches the given exception type, use the rule to get the name of the DBus error. The rules in the error mapper are processed in the reversed order to respect the priority of the rules. :param exception_type: a type of the Python error :type exception_type: a subclass of Exception :return: a name of the DBus error :raise LookupError: if no name is found """ for rule in reversed(self._error_rules): if rule.match_type(exception_type): return rule.get_name(exception_type) raise LookupError( "No name found for '{}'.".format(exception_type.__name__) ) def get_exception_type(self, error_name): """Get a Python exception type of the DBus error. Try to find a matching rule in the error mapper. If a rule matches the given name of a DBus error, use the rule to get the type of a Python exception. The rules in the error mapper are processed in the reversed order to respect the priority of the rules. :param error_name: a name of the DBus error :return: a type of the Python exception :rtype: a subclass of Exception :raise LookupError: if no type is found """ for rule in reversed(self._error_rules): if rule.match_name(error_name): return rule.get_type(error_name) raise LookupError("No type found for '{}'.".format(error_name)) dasbus-1.7/dasbus/identifier.py000066400000000000000000000145671433220137200166310ustar00rootroot00000000000000# # Identification of DBus objects, interfaces and services # # Copyright (C) 2019 Red Hat, Inc. All rights reserved. # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 # USA # from dasbus.namespace import get_dbus_path, get_dbus_name __all__ = [ 'DBusInterfaceIdentifier', 'DBusObjectIdentifier', 'DBusServiceIdentifier' ] class DBusBaseIdentifier(object): """A base identifier.""" def __init__(self, namespace, basename=None): """Create an identifier. :param namespace: a sequence of strings :param basename: a string with the base name or None """ if basename: namespace = (*namespace, basename) self._namespace = namespace self._name = get_dbus_name(*namespace) self._path = get_dbus_path(*namespace) @property def namespace(self): """DBus namespace of this object.""" return self._namespace def __str__(self): """Return the string representation.""" return self._name class DBusInterfaceIdentifier(DBusBaseIdentifier): """Identifier of a DBus interface.""" def __init__(self, namespace, basename=None, interface_version=None): """Describe a DBus interface. :param namespace: a sequence of strings :param basename: a string with the base name or None :param interface_version: a version of the interface """ super().__init__(namespace, basename=basename) self._interface_version = interface_version def _version_to_string(self, version): """Convert version to a string. :param version: a number or None :return: a string """ if version is None: return "" return str(version) @property def interface_name(self): """Full name of the DBus interface.""" return self._name + self._version_to_string(self._interface_version) def __str__(self): """Return the string representation.""" return self.interface_name class DBusObjectIdentifier(DBusInterfaceIdentifier): """Identifier of a DBus object.""" def __init__(self, namespace, basename=None, interface_version=None, object_version=None): """Describe a DBus object. :param namespace: a sequence of strings :param basename: a string with the base name or None :param interface_version: a version of the DBus interface :param object_version: a version of the DBus object """ super().__init__(namespace, basename=basename, interface_version=interface_version) self._object_version = object_version @property def object_path(self): """Full path of the DBus object.""" return self._path + self._version_to_string(self._object_version) def __str__(self): """Return the string representation.""" return self.object_path class DBusServiceIdentifier(DBusObjectIdentifier): """Identifier of a DBus service.""" def __init__(self, message_bus, namespace, basename=None, interface_version=None, object_version=None, service_version=None): """Describe a DBus service. :param message_bus: a message bus :param namespace: a sequence of strings :param basename: a string with the base name or None :param interface_version: a version of the DBus interface :param object_version: a version of the DBus object :param service_version: a version of the DBus service """ super().__init__(namespace, basename=basename, interface_version=interface_version, object_version=object_version) self._service_version = service_version self._message_bus = message_bus @property def message_bus(self): """Message bus of the DBus service. :return: a message bus :rtype: an instance of the MessageBus class """ return self._message_bus @property def service_name(self): """Full name of a DBus service.""" return self._name + self._version_to_string(self._service_version) def __str__(self): """Return the string representation.""" return self.service_name def _choose_object_path(self, object_id): """Choose an object path.""" if object_id is None: return self.object_path if isinstance(object_id, DBusObjectIdentifier): return object_id.object_path return object_id def _choose_interface_name(self, interface_id): """Choose an interface name.""" if interface_id is None: return None if isinstance(interface_id, DBusInterfaceIdentifier): return interface_id.interface_name return interface_id def get_proxy(self, object_path=None, interface_name=None, **bus_arguments): """Returns a proxy of the DBus object. If no object path is specified, we will use the object path of this DBus service. If no interface name is specified, we will use none and create a proxy from all interfaces of the DBus object. :param object_path: an object identifier or a DBus path or None :param interface_name: an interface identifier or a DBus name or None :param bus_arguments: additional arguments for the message bus :return: a proxy object """ object_path = self._choose_object_path(object_path) interface_name = self._choose_interface_name(interface_name) return self._message_bus.get_proxy( self.service_name, object_path, interface_name, **bus_arguments ) dasbus-1.7/dasbus/loop.py000066400000000000000000000036371433220137200154540ustar00rootroot00000000000000# # Representation of an event loop # # Copyright (C) 2020 Red Hat, Inc. All rights reserved. # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 # USA # from abc import ABCMeta, abstractmethod import gi gi.require_version("GLib", "2.0") from gi.repository import GLib __all__ = [ "AbstractEventLoop", "EventLoop" ] class AbstractEventLoop(metaclass=ABCMeta): """The abstract representation of the event loop. It is necessary to run the event loop to handle emitted DBus signals or incoming DBus calls (in the DBus service). Example: .. code-block:: python # Create the event loop. loop = EventLoop() # Start the event loop. loop.run() # Run loop.quit() to stop. """ @abstractmethod def run(self): """Start the event loop.""" pass @abstractmethod def quit(self): """Stop the event loop.""" pass class EventLoop(AbstractEventLoop): """The representation of the event loop.""" def __init__(self): """Create the event loop.""" self._loop = GLib.MainLoop() def run(self): """Start the event loop.""" self._loop.run() def quit(self): """Stop the event loop.""" self._loop.quit() dasbus-1.7/dasbus/namespace.py000066400000000000000000000027001433220137200164250ustar00rootroot00000000000000# # DBus names and paths # # Copyright (C) 2019 Red Hat, Inc. All rights reserved. # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 # USA# __all__ = [ "get_dbus_name", "get_dbus_path", "get_namespace_from_name" ] def get_dbus_name(*namespace): """Create a DBus name from the given names. :param namespace: a sequence of names :return: a DBus name """ return ".".join(namespace) def get_dbus_path(*namespace): """Create a DBus path from the given names. :param namespace: a sequence of names :return: a DBus path """ return "/" + "/".join(namespace) def get_namespace_from_name(name): """Return a namespace of the DBus name. :param name: a DBus name :return: a sequence of names """ return tuple(name.split(".")) dasbus-1.7/dasbus/server/000077500000000000000000000000001433220137200154265ustar00rootroot00000000000000dasbus-1.7/dasbus/server/__init__.py000066400000000000000000000000001433220137200175250ustar00rootroot00000000000000dasbus-1.7/dasbus/server/container.py000066400000000000000000000140301433220137200177600ustar00rootroot00000000000000# # Server support for DBus containers # # Copyright (C) 2019 Red Hat, Inc. All rights reserved. # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 # USA # from dasbus.namespace import get_dbus_path from dasbus.server.publishable import Publishable from dasbus.typing import ObjPath, List __all__ = [ "DBusContainerError", "DBusContainer" ] class DBusContainerError(Exception): """General exception for DBus container errors.""" pass class DBusContainer(object): """The container of DBus objects. A DBus container should be used to dynamically publish Publishable objects within the same namespace. It generates a unique DBus path for each object. It is able to resolve a DBus path into an object and an object into a DBus path. Example: .. code-block:: python # Create a container of tasks. container = DBusContainer( namespace=("my", "project"), basename="Task", message_bus=DBus ) # Publish a task. path = container.to_object_path(MyTask()) # Resolve an object path into a task. task = container.from_object_path(path) """ def __init__(self, message_bus, namespace, basename=None): """Create a new container. :param message_bus: a message bus :param namespace: a sequence of names :param basename: a string with the base name """ self._message_bus = message_bus if basename: namespace = (*namespace, basename) self._namespace = namespace[:-1] self._basename = namespace[-1] self._container = {} self._published = set() self._counter = 0 def set_namespace(self, namespace): """Set the namespace. All DBus objects from the container should use the same namespace, so the namespace should be set up before any of the DBus objects are published. :param namespace: a sequence of names """ self._namespace = namespace def from_object_path(self, object_path: ObjPath): """Convert a DBus path to a published object. If no published object is found for the given DBus path, raise DBusContainerError. :param object_path: a DBus path :return: a published object """ return self._find_object(object_path) def to_object_path(self, obj) -> ObjPath: """Convert a publishable object to a DBus path. If no DBus path is found for the given object, publish the object on the container message bus with a unique DBus path generated from the container namespace. :param obj: a publishable object :return: a DBus path """ if not isinstance(obj, Publishable): raise TypeError( "Type '{}' is not publishable.".format(type(obj).__name__) ) if not self._is_object_published(obj): self._publish_object(obj) return self._find_object_path(obj) def from_object_path_list(self, object_paths: List[ObjPath]): """Convert DBus paths to published objects. :param object_paths: a list of DBus paths :return: a list of published objects """ return list(map(self.from_object_path, object_paths)) def to_object_path_list(self, objects) -> List[ObjPath]: """Convert publishable objects to DBus paths. :param objects: a list of publishable objects :return: a list of DBus paths """ return list(map(self.to_object_path, objects)) def _is_object_published(self, obj): """Is the given object published? :param obj: an object :return: True if the object is published, otherwise False """ return id(obj) in self._published def _publish_object(self, obj: Publishable): """Publish the given object. :param obj: an object to publish :return: an object path """ object_path = self._generate_object_path() self._message_bus.publish_object( object_path, obj.for_publication() ) self._container[object_path] = obj self._published.add(id(obj)) return object_path def _find_object_path(self, obj): """Find a DBus path of the object. :param obj: a published object :return: a DBus path :raise: DBusContainerError if no object path is found """ for object_path, found_obj in self._container.items(): if found_obj is obj: return object_path raise DBusContainerError( "No object path found." ) def _find_object(self, object_path): """Find an object by its DBus path. :param object_path: a DBus path :return: a published object :raise: DBusContainerError if no object is found """ if object_path in self._container: return self._container[object_path] raise DBusContainerError( "Unknown object path '{}'.".format(object_path) ) def _generate_object_path(self): """Generate a unique object path. This method is not thread safe. :return: a unique object path """ self._counter += 1 return get_dbus_path( *self._namespace, self._basename, str(self._counter) ) dasbus-1.7/dasbus/server/handler.py000066400000000000000000000461671433220137200174330ustar00rootroot00000000000000# # Server support for DBus objects # # Copyright (C) 2019 Red Hat, Inc. All rights reserved. # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 # USA # import logging from abc import ABCMeta, abstractmethod from functools import partial from dasbus.error import ErrorMapper from dasbus.signal import Signal from dasbus.server.interface import get_xml, are_additional_arguments_supported from dasbus.specification import DBusSpecification, DBusSpecificationError from dasbus.typing import get_variant, unwrap_variant, is_tuple_of_one import gi gi.require_version("Gio", "2.0") from gi.repository import Gio log = logging.getLogger(__name__) __all__ = [ "GLibServer", "AbstractServerObjectHandler", "ServerObjectHandler" ] class GLibServer(object): """The low-level DBus server library based on GLib.""" @classmethod def emit_signal(cls, connection, object_path, interface_name, signal_name, parameters, destination=None): """Emit a DBus signal.""" connection.emit_signal( destination, object_path, interface_name, signal_name, parameters ) @classmethod def register_object(cls, connection, object_path, object_xml, callback, callback_args=()): """Register an object on DBus.""" node_info = Gio.DBusNodeInfo.new_for_xml( object_xml ) method_call_closure = partial( cls._object_callback, user_data=(callback, callback_args) ) registrations = [] if not node_info.interfaces: raise DBusSpecificationError( "No DBus interfaces for registration." ) for interface_info in node_info.interfaces: registration_id = connection.register_object( object_path, interface_info, method_call_closure, None, None ) registrations.append(registration_id) return partial( cls._unregister_object, connection, registrations ) @classmethod def _unregister_object(cls, connection, registrations): """Unregister an object from DBus.""" for registration_id in registrations: connection.unregister_object(registration_id) @classmethod def _object_callback(cls, connection, sender, object_path, interface_name, method_name, parameters, invocation, user_data): # Prepare the user's callback. callback, callback_args = user_data # Call user's callback. callback( invocation, interface_name, method_name, parameters, *callback_args ) @classmethod def get_call_info(cls, invocation): """Get information about the DBus call. Supported items: sender str: The bus name that invoked the method There can be more supported items in the future. :param invocation: an invocation of a DBus call :return: a dictionary of information about the DBus call """ return { "sender": invocation.get_sender() } @classmethod def set_call_error(cls, invocation, error_name, error_message): """Set the error of the DBus call. :param invocation: an invocation of a DBus call :param error_name: a DBus name of the error :param error_message: an error message """ invocation.return_dbus_error(error_name, error_message) @classmethod def set_call_reply(cls, invocation, out_type, out_value): """Set the reply of the DBus call. :param invocation: an invocation of a DBus call :param out_type: a type of the reply :param out_value: a value of the reply """ reply_value = cls._get_reply_value(out_type, out_value) invocation.return_value(reply_value) @classmethod def _get_reply_value(cls, out_type, out_value): """Get the reply value of the DBus call.""" if out_type is None: return None if is_tuple_of_one(out_type): out_value = (out_value, ) return get_variant(out_type, out_value) class AbstractServerObjectHandler(metaclass=ABCMeta): """The abstract handler of a published object.""" __slots__ = [ "_message_bus", "_object_path", "_object", "_specification" ] def __init__(self, message_bus, object_path, obj): """Create a new handler. :param message_bus: a message bus :param object_path: a DBus path of the object :param obj: a Python instance of the object """ self._message_bus = message_bus self._object_path = object_path self._object = obj self._specification = None @property def specification(self): """DBus specification.""" if not self._specification: self._specification = self._get_specification() return self._specification def _get_specification(self): """Get the DBus specification. :return: a DBus specification """ return DBusSpecification.from_xml( self._get_xml_specification() ) @abstractmethod def _get_xml_specification(self): """Get the XML specification. :return: a XML specification """ return "" @abstractmethod def connect_object(self): """Connect the object to DBus. Handle emitted signals of the object with the _emit_signal method and handle incoming DBus calls with the _handle_call method. """ pass @abstractmethod def disconnect_object(self): """Disconnect the object from DBus. Unregister the object and disconnect all signals. """ pass @abstractmethod def _connect_signal(self, interface_name, signal_name): """Connect a DBus signal. :param interface_name: a DBus interface name :param signal_name: a DBus signal name """ pass @abstractmethod def _emit_signal(self, interface_name, signal_name, *parameters): """Handle a DBus signal. :param interface_name: a DBus interface name :param signal_name: a DBus name of the signal :param parameters: a signal parameters """ pass def _handle_call(self, interface_name, method_name, *parameters, **additional_args): """Handle a DBus call. :param interface_name: a name of the interface :param method_name: a name of the called method :param parameters: parameters of the call :param additional_args: additional arguments of the call :return: a result of the DBus call """ handler = self._find_handler(interface_name, method_name) # Drop the extra args if the handler doesn't support them. if not are_additional_arguments_supported(handler): additional_args = {} return handler(*parameters, **additional_args) def _find_member_spec(self, interface_name, member_name): """Find a specification of the DBus member. :param interface_name: a name of the interface :param member_name: a name of the member :return: a specification of the member """ return self.specification.get_member( interface_name, member_name ) def _find_handler(self, interface_name, member_name): """Find a handler of a DBus member. :param interface_name: a name of the interface :param member_name: a name of the method :return: a handler """ handler = self._find_object_handler(interface_name, member_name) \ or self._find_default_handler(interface_name, member_name) if not handler: raise AttributeError("The member {}.{} has no handler.".format( interface_name, member_name )) return handler def _find_object_handler(self, interface_name, member_name): """Get an object handler of a DBus call. By default, DBus interfaces with members of the same name are not supported, so the given interface name is not used to find the object handler. :param interface_name: a name of the interface. :param member_name: a name of the member :return: a handler or None """ return getattr(self._object, member_name, None) @abstractmethod def _find_default_handler(self, interface_name, member_name): """Find a default handler of a DBus call. :param interface_name: a name of the interface :param member_name: a name of the member :return: a handler or None """ pass class ServerObjectHandler(AbstractServerObjectHandler): """The handler of an object published on DBus.""" __slots__ = [ "_server", "_signal_factory", "_error_mapper", "_registrations" ] def __init__(self, message_bus, object_path, obj, error_mapper=None, server=GLibServer, signal_factory=Signal): """Create a new handler. :param message_bus: a message bus :param object_path: a DBus path of the object :param obj: a Python instance of the object :param error_mapper: a DBus error mapper :param server: a DBus server library :param signal_factory: a signal factory """ super().__init__(message_bus, object_path, obj) self._server = server self._signal_factory = signal_factory self._error_mapper = error_mapper or ErrorMapper() self._registrations = [] def _get_xml_specification(self): """Get the XML specification. :return: a XML specification """ return get_xml(self._object) def connect_object(self): """Connect the object to DBus.""" self._register_object() self._connect_signals() def disconnect_object(self): """Disconnect the object from DBus.""" while self._registrations: callback = self._registrations.pop() callback() def _register_object(self): """Register to DBus calls. :return: an unregistering callback """ unregister = self._server.register_object( self._message_bus.connection, self._object_path, self._get_xml_specification(), self._method_callback ) self._registrations.append(unregister) def _connect_signals(self): """Connect all DBus signals.""" for member in self.specification.members: if not isinstance(member, DBusSpecification.Signal): continue self._connect_signal( member.interface_name, member.name ) def _connect_signal(self, interface_name, signal_name): """Connect a DBus signal. :param interface_name: a DBus interface name :param signal_name: a DBus signal name :return: a disconnecting callback """ callback = self._find_emitter(interface_name, signal_name) signal = self._find_handler(interface_name, signal_name) signal.connect(callback) disconnect = partial(signal.disconnect, callback) self._registrations.append(disconnect) def _find_emitter(self, interface_name, signal_name): """Find an emitter of a DBus signal. :param interface_name: a DBus interface name :param signal_name: a DBus signal name :return: a callback """ return partial(self._emit_signal, interface_name, signal_name) def _emit_signal(self, interface_name, signal_name, *parameters): """Handle a DBus signal. :param interface_name: a DBus interface name :param signal_name: a DBus signal name :param parameters: a signal parameters """ member = self._find_member_spec(interface_name, signal_name) if not parameters: parameters = None if member.type is not None: parameters = get_variant(member.type, parameters) self._server.emit_signal( self._message_bus.connection, self._object_path, interface_name, signal_name, parameters ) def _method_callback(self, invocation, interface_name, method_name, parameters): """The callback for a DBus call. :param invocation: an invocation of the DBus call :param interface_name: a DBus interface name :param method_name: a DBus method name :param parameters: a variant of DBus arguments """ try: additional_args = self._get_additional_arguments( invocation, interface_name, method_name, parameters ) member = self._find_member_spec( interface_name, method_name ) result = self._handle_call( interface_name, method_name, *unwrap_variant(parameters), **additional_args ) self._handle_method_result( invocation, member, result ) except Exception as error: # pylint: disable=broad-except self._handle_method_error( invocation, interface_name, method_name, error ) def _get_additional_arguments(self, invocation, interface_name, method_name, parameters): """Get additional arguments of a DBus call. Supported items: call_info dict: Information about the DBus call This method is useful for customizations. It shouldn't be changed in this library unless we make sure that the change won't break the existing use cases. :param invocation: an invocation of the DBus call :param interface_name: a DBus interface name :param method_name: a DBus method name :param parameters: a variant of DBus arguments :return: a dictionary of additional info """ return { "call_info": self._server.get_call_info(invocation) } def _handle_method_error(self, invocation, interface_name, method_name, error): """Handle an error of a DBus call. :param invocation: an invocation of the DBus call :param interface_name: a DBus interface name :param method_name: a DBus method name :param error: an exception raised during the call """ log.warning( "The call %s.%s has failed with an exception:", interface_name, method_name, exc_info=True ) error_name = self._error_mapper.get_error_name( type(error) ) self._server.set_call_error( invocation, error_name, str(error) ) def _handle_method_result(self, invocation, method_spec, method_reply): """Handle a result of a DBus call. :param invocation: an invocation of a DBus call :param method_spec: a method specification :param method_reply: a method reply """ self._server.set_call_reply( invocation, method_spec.out_type, method_reply ) def _find_default_handler(self, interface_name, member_name): """Find a default handler of a DBus call. :param interface_name: a name of the interface :param member_name: a name of the member :return: a handler or None """ if interface_name == "org.freedesktop.DBus.Properties": if member_name == "Get": return self._get_property elif member_name == "Set": return self._set_property elif member_name == "GetAll": return self._get_all_properties elif member_name == "PropertiesChanged": return self._properties_changed return None def _get_property(self, interface_name, property_name): """The default handler of the Get method. :param interface_name: an interface name :param property_name: a property name :return: a variant with a property value """ member = self._find_member_spec(interface_name, property_name) if not member.readable: raise AttributeError("The property {}.{} is not readable.".format( interface_name, property_name )) value = getattr(self._object, property_name) return get_variant(member.type, value) def _set_property(self, interface_name, property_name, property_value): """The default handler of the Set method. :param interface_name: an interface name :param property_name: a property name :param property_value: a variant with a property value """ member = self._find_member_spec(interface_name, property_name) if not member.writable: raise AttributeError("The property {}.{} is not writable.".format( interface_name, property_name )) setattr(self._object, property_name, unwrap_variant(property_value)) def _find_all_properties(self, interface_name): """Find all properties of the given interface. :param interface_name: an interface name :return: a list of property names """ return [ member.name for member in self.specification.members if isinstance(member, DBusSpecification.Property) and member.interface_name == interface_name and member.readable ] def _get_all_properties(self, interface_name): """The default handler of the GetAll method. :param interface_name: an interface name :return: a dictionary of properties """ return { property_name: self._get_property(interface_name, property_name) for property_name in self._find_all_properties(interface_name) } @property def _properties_changed(self): """The default handler of the PropertiesChanged method. :return: a signal """ return self._signal_factory() dasbus-1.7/dasbus/server/interface.py000066400000000000000000000533101433220137200177420ustar00rootroot00000000000000# # Server support for DBus interfaces # # Copyright (C) 2019 Red Hat, Inc. All rights reserved. # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 # USA # # For more info about DBus specification see: # https://dbus.freedesktop.org/doc/dbus-specification.html#introspection-format # import inspect import re from inspect import Parameter from typing import get_type_hints from dasbus.namespace import get_dbus_name from dasbus.signal import Signal from dasbus.specification import DBusSpecificationError, DBusSpecification from dasbus.typing import get_dbus_type, is_base_type, get_type_arguments, \ Tuple from dasbus.xml import XMLGenerator __all__ = [ "dbus_class", "dbus_interface", "dbus_signal", "get_xml", "accepts_additional_arguments", "are_additional_arguments_supported" ] # Class attribute for the XML specification. DBUS_XML_ATTRIBUTE = "__dbus_xml__" # Method attribute for the @returns_multiple_arguments decorator. RETURNS_MULTIPLE_ARGUMENTS_ATTRIBUTE = \ "__dbus_method_returns_multiple_arguments__" # Method attribute for the @accepts_additional_arguments decorator. ACCEPTS_ADDITIONAL_ARGUMENTS_ATTRIBUTE = \ "__dbus_handler_accepts_additional_arguments__" def returns_multiple_arguments(method): """Decorator for returning multiple arguments from a DBus method. The decorator allows to generate multiple output arguments in the XML specification of the decorated DBus method. Otherwise, there will be only one output argument in the specification. Define a DBus method with multiple output arguments: .. code-block:: python @returns_multiple_arguments def Method(self) -> Tuple[Int, Bool]: return 0, False The generated XML specification of the example: .. code-block:: xml If the XML specification is not generated by the dbus_interface decorator, the returns_multiple_arguments decorator has no effect. :param method: a DBus method :return: a DBus method with a flag """ setattr(method, RETURNS_MULTIPLE_ARGUMENTS_ATTRIBUTE, True) return method def accepts_additional_arguments(method): """Decorator for accepting extra arguments in a DBus method. The decorator allows the server object handler to propagate additional information about the DBus call into the decorated method. Use a dictionary of keyword arguments: .. code-block:: python @accepts_additional_arguments def Method(x: Int, y: Str, **info): pass Or use keyword only parameters: .. code-block:: python @accepts_additional_arguments def Method(x: Int, y: Str, *, call_info): pass At this moment, the library provides only the call_info argument generated by GLibServer.get_call_info, but the additional arguments can be customized in the _get_additional_arguments method of the server object handler. :param method: a DBus method :return: a DBus method with a flag """ setattr(method, ACCEPTS_ADDITIONAL_ARGUMENTS_ATTRIBUTE, True) return method def are_additional_arguments_supported(method): """Does the given DBus method accept additional arguments? :param method: a DBus method :return: True or False """ return getattr(method, ACCEPTS_ADDITIONAL_ARGUMENTS_ATTRIBUTE, False) class dbus_signal(object): """DBus signal. Can be used as: .. code-block:: python Signal = dbus_signal() Or as a method decorator: .. code-block:: python @dbus_signal def Signal(x: Int, y: Double): pass Signal is defined by the type hints of a decorated method. This method is accessible as: signal.definition If the signal is not defined by a method, it is expected to have no arguments and signal.definition is equal to None. """ def __init__(self, definition=None, factory=Signal): """Create a signal descriptor. :param definition: a definition of the emit function :param factory: a signal factory """ self.definition = definition self.factory = factory self.name = None def __set_name__(self, owner, name): """Set a name of the descriptor The descriptor has been assigned to the specified name. Generate a name of a private attribute that will be set to a signal in the ``__get__`` method. For example: ``__dbus_signal_my_name`` :param owner: the owning class :param name: the descriptor name """ if self.name is not None: return self.name = "__{}_{}".format( type(self).__name__.lower(), name.lower() ) def __get__(self, instance, owner): """Get a value of the descriptor. If the descriptor is accessed as a class attribute, return the descriptor. If the descriptor is accessed as an instance attribute, return a signal created by the signal factory. :param instance: an instance of the owning class :param owner: an owning class :return: a value of the attribute """ if instance is None: return self signal = getattr(instance, self.name, None) if signal is None: signal = self.factory() setattr(instance, self.name, signal) return signal def __set__(self, instance, value): """Set a value of the descriptor.""" raise AttributeError("Can't set DBus signal.") def dbus_interface(interface_name, namespace=()): """DBus interface. A new DBus interface can be defined as: .. code-block:: python @dbus_interface class Interface(): ... The interface will be generated from the given class cls with a name interface_name and added to the DBus XML specification of the class. The XML specification is accessible as: .. code-block:: python Interface.__dbus_xml__ It is conventional for member names on DBus to consist of capitalized words with no punctuation. The generator of the XML specification enforces this convention to prevent unintended changes in the specification. You can provide the XML specification yourself, or override the generator class to work around these constraints. :param interface_name: a DBus name of the interface :param namespace: a sequence of strings """ def decorated(cls): name = get_dbus_name(*namespace, interface_name) xml = DBusSpecificationGenerator.generate_specification(cls, name) setattr(cls, DBUS_XML_ATTRIBUTE, xml) return cls return decorated def dbus_class(cls): """DBus class. A new DBus class can be defined as: .. code-block:: python @dbus_class class Class(Interface): ... DBus class can implement DBus interfaces, but it cannot define a new interface. The DBus XML specification will be generated from implemented interfaces (inherited) and it will be accessible as: .. code-block:: python Class.__dbus_xml__ """ xml = DBusSpecificationGenerator.generate_specification(cls) setattr(cls, DBUS_XML_ATTRIBUTE, xml) return cls def get_xml(obj): """Return XML specification of an object. :param obj: an object decorated with @dbus_interface or @dbus_class :return: a string with XML specification """ xml_specification = getattr(obj, DBUS_XML_ATTRIBUTE, None) if xml_specification is None: raise DBusSpecificationError( "XML specification is not defined at '{}'.".format( DBUS_XML_ATTRIBUTE ) ) return xml_specification class DBusSpecificationGenerator(object): """Class for generating DBus XML specification.""" # The XML generator. xml_generator = XMLGenerator # The pattern of a DBus member name. NAME_PATTERN = re.compile(r'[A-Z][A-Za-z0-9]*') @classmethod def generate_specification(cls, interface_cls, interface_name=None): """Generates DBus XML specification for given class. If class defines a new interface, it will be added to the specification. :param interface_cls: class object to decorate :param str interface_name: name of the interface defined by class :return str: DBus specification in XML """ # Collect all interfaces that class inherits. interfaces = cls._collect_interfaces(interface_cls) # Generate a new interface. if interface_name: all_interfaces = cls._collect_standard_interfaces() all_interfaces.update(interfaces) interface = cls._generate_interface( interface_cls, all_interfaces, interface_name ) interfaces[interface_name] = interface # Generate XML specification for the given class. node = cls._generate_node(interface_cls, interfaces) return cls.xml_generator.element_to_xml(node) @classmethod def _collect_standard_interfaces(cls): """Collect standard interfaces. Standard interfaces are implemented by default. :return: a dictionary of standard interfaces """ node = cls.xml_generator.xml_to_element( DBusSpecification.STANDARD_INTERFACES ) return cls.xml_generator.get_interfaces_from_node(node) @classmethod def _collect_interfaces(cls, interface_cls): """Collect interfaces implemented by the class. Returns a dictionary that maps interface names to interface elements. :param interface_cls: a class object :return: a dictionary of implemented interfaces """ interfaces = {} # Visit interface_cls and base classes in reversed order. for member in reversed(inspect.getmro(interface_cls)): # Skip classes with no specification. member_xml = getattr(member, DBUS_XML_ATTRIBUTE, None) if not member_xml: continue # Update found interfaces. node = cls.xml_generator.xml_to_element(member_xml) node_interfaces = cls.xml_generator.get_interfaces_from_node(node) interfaces.update(node_interfaces) return interfaces @classmethod def _generate_interface(cls, interface_cls, interfaces, interface_name): """Generate interface defined by given class. :param interface_cls: a class object that defines the interface :param interfaces: a dictionary of implemented interfaces :param interface_name: a name of the new interface :return: a new interface element :raises DBusSpecificationError: if a class member cannot be exported """ interface = cls.xml_generator.create_interface(interface_name) # Search class members. for member_name, member in inspect.getmembers(interface_cls): # Check it the name is exportable. if not cls._is_exportable(member_name): continue # Skip names already defined in implemented interfaces. if cls._is_defined(interfaces, member_name): continue # Generate XML element for exportable member. if cls._is_signal(member): element = cls._generate_signal(member, member_name) elif cls._is_property(member): element = cls._generate_property(member, member_name) elif cls._is_method(member): element = cls._generate_method(member, member_name) else: raise DBusSpecificationError( "Unsupported definition of DBus member '{}'.".format( member_name ) ) # Add generated element to the interface. cls.xml_generator.add_child(interface, element) return interface @classmethod def _is_exportable(cls, member_name): """Is the name of a class member exportable? The name is exportable if it follows the DBus specification. Only CamelCase names are allowed. """ return bool(cls.NAME_PATTERN.fullmatch(member_name)) @classmethod def _is_defined(cls, interfaces, member_name): """Is the member name defined in given interfaces? :param interfaces: a dictionary of interfaces :param member_name: a name of the class member :return: True if the name is defined, otherwise False """ for interface in interfaces.values(): for member in interface: # Is it a signal, a property or a method? if not cls.xml_generator.is_member(member): continue # Does it have the same name? if not cls.xml_generator.has_name(member, member_name): continue # The member is already defined. return True return False @classmethod def _is_signal(cls, member): """Is the class member a DBus signal?""" return isinstance(member, dbus_signal) @classmethod def _generate_signal(cls, member, member_name): """Generate signal defined by a class member. :param member: a dbus_signal object. :param member_name: a name of the signal :return: a signal element raises DBusSpecificationError: if signal has defined return type """ element = cls.xml_generator.create_signal(member_name) method = member.definition if not method: return element for name, type_hint, direction in cls._iterate_parameters(method): # Only input parameters can be defined. if direction == DBusSpecification.DIRECTION_OUT: raise DBusSpecificationError( "Invalid return type of DBus signal " "'{}'.".format(member_name) ) # All parameters are exported as output parameters # (see specification). direction = DBusSpecification.DIRECTION_OUT parameter = cls.xml_generator.create_parameter( name, get_dbus_type(type_hint), direction ) cls.xml_generator.add_child(element, parameter) return element @classmethod def _iterate_parameters(cls, member): """Iterate over method parameters. For every parameter returns its name, a type hint and a direction. :param member: a method object :return: an iterator raises DBusSpecificationError: if parameters are invalid """ signature = inspect.signature(member) yield from cls._iterate_in_parameters(member, signature) yield from cls._iterate_out_parameters(member, signature) @classmethod def _iterate_in_parameters(cls, member, signature): """Iterate over input parameters.""" # Get type hints for parameters. direction = DBusSpecification.DIRECTION_IN type_hints = get_type_hints(member) # Iterate over method parameters, skip cls. for name in list(signature.parameters)[1:]: # Check the kind of the parameter kind = signature.parameters[name].kind # Ignore **kwargs and all arguments after * and *args # if the method supports additional arguments. if kind in (Parameter.VAR_KEYWORD, Parameter.KEYWORD_ONLY) \ and are_additional_arguments_supported(member): continue if kind != Parameter.POSITIONAL_OR_KEYWORD: raise DBusSpecificationError( "Only positional or keyword arguments are allowed." ) # Check if the type is defined. if name not in type_hints: raise DBusSpecificationError( "Undefined type of parameter '{}'.".format(name) ) yield name, type_hints[name], direction @classmethod def _iterate_out_parameters(cls, member, signature): """Iterate over output parameters.""" name = DBusSpecification.RETURN_PARAMETER direction = DBusSpecification.DIRECTION_OUT type_hint = signature.return_annotation # Is the return type defined? if type_hint is signature.empty: return # Is the return type other than None? if type_hint is None: return # Generate multiple output arguments if requested. if getattr(member, RETURNS_MULTIPLE_ARGUMENTS_ATTRIBUTE, False): # The return type has to be a tuple. if not is_base_type(type_hint, Tuple): raise DBusSpecificationError( "Expected a tuple of multiple arguments." ) # The return type has to contain multiple arguments. type_args = get_type_arguments(type_hint) if len(type_args) < 2: raise DBusSpecificationError( "Expected a tuple of more than one argument." ) # Iterate over types in the tuple for i, type_arg in enumerate(type_args): yield "{}_{}".format(name, i), type_arg, direction return # Otherwise, return only one output argument. yield name, type_hint, direction @classmethod def _is_property(cls, member): """Is the class member a DBus property?""" return isinstance(member, property) @classmethod def _generate_property(cls, member, member_name): """Generate DBus property defined by class member. :param member: a property object :param member_name: a property name :return: a property element raises DBusSpecificationError: if the property is invalid """ access = None type_hint = None try: # Process the setter. if member.fset: [(_, type_hint, _)] = cls._iterate_parameters(member.fset) access = DBusSpecification.ACCESS_WRITE # Process the getter. if member.fget: [(_, type_hint, _)] = cls._iterate_parameters(member.fget) access = DBusSpecification.ACCESS_READ except ValueError: raise DBusSpecificationError( "Undefined type of DBus property '{}'.".format(member_name) ) from None # Property has both. if member.fget and member.fset: access = DBusSpecification.ACCESS_READWRITE if access is None: raise DBusSpecificationError( "DBus property '{}' is not accessible.".format(member_name) ) return cls.xml_generator.create_property( member_name, get_dbus_type(type_hint), access ) @classmethod def _is_method(cls, member): """Is the class member a DBus method? Ignore the difference between instance method and class method. For example: .. code-block:: python class Foo(object): def bar(cls, x): pass inspect.isfunction(Foo.bar) # True inspect.isfunction(Foo().bar) # False inspect.ismethod(Foo.bar) # False inspect.ismethod(Foo().bar) # True _is_method(Foo.bar) # True _is_method(Foo().bar) # True """ return inspect.ismethod(member) or inspect.isfunction(member) @classmethod def _generate_method(cls, member, member_name): """Generate method defined by given class member. :param member: a method object :param member_name: a name of the method :return: a method element """ method = cls.xml_generator.create_method(member_name) # Process the parameters. for name, type_hint, direction in cls._iterate_parameters(member): # Create the parameter element. parameter = cls.xml_generator.create_parameter( name, get_dbus_type(type_hint), direction ) # Add the element to the method element. cls.xml_generator.add_child(method, parameter) return method @classmethod def _generate_node(cls, interface_cls, interfaces): """Generate node element that specifies the given class. :param interface_cls: a class object :param interfaces: a dictionary of interfaces :return: a node element """ node = cls.xml_generator.create_node() # Add comment about specified class. cls.xml_generator.add_comment( node, "Specifies {}".format(interface_cls.__name__) ) # Add interfaces sorted by their names. for interface_name in sorted(interfaces.keys()): cls.xml_generator.add_child(node, interfaces[interface_name]) return node dasbus-1.7/dasbus/server/property.py000066400000000000000000000140011433220137200176600ustar00rootroot00000000000000# # Server support for DBus properties # # Copyright (C) 2019 Red Hat, Inc. All rights reserved. # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 # USA # # For more info about DBus specification see: # https://dbus.freedesktop.org/doc/dbus-specification.html#introspection-format # from abc import ABCMeta from collections import defaultdict from functools import wraps from typing import Dict from dasbus.server.interface import dbus_signal, get_xml from dasbus.specification import DBusSpecification, DBusSpecificationError from dasbus.typing import get_variant, Str, Variant, List __all__ = [ "emits_properties_changed", "PropertiesException", "PropertiesInterface" ] def emits_properties_changed(method): """Decorator for emitting properties changes. The decorated method has to be a member of a class that inherits PropertiesInterface. :param method: a DBus method of a class that inherits PropertiesInterface :return: a wrapper of a DBus method that emits PropertiesChanged """ @wraps(method) def wrapper(obj, *args, **kwargs): result = method(obj, *args, **kwargs) obj.flush_changes() return result return wrapper class PropertiesException(Exception): """Exception for DBus properties.""" pass class PropertiesChanges(object): """Cache for properties changes. This class is useful to collect the changed properties and their values, before they are emitted on DBus. """ def __init__(self, obj): """Create the cache. :param obj: an object with DBus properties """ self._object = obj self._properties_names = set() self._properties_specs = self._find_properties_specs(obj) def _find_properties_specs(self, obj): """Find specifications of DBus properties. :param obj: an object with DBus properties :return: a map of property names and their specifications """ specification = DBusSpecification.from_xml(get_xml(obj)) properties_specs = {} for member in specification.members: if not isinstance(member, DBusSpecification.Property): continue if member.name in properties_specs: raise DBusSpecificationError( "DBus property '{}' is defined in more than " "one interface.".format(member.name) ) properties_specs[member.name] = member return properties_specs def flush(self): """Flush the cache. The content of the cache will be composed to requests and the cache will be cleared. The requests can be used to emit the PropertiesChanged signal. The requests are a list of tuples, that contain an interface name and a dictionary of properties changes. :return: a list of requests """ content = self._properties_names self._properties_names = set() requests = defaultdict(dict) for property_name in content: # Find the property specification. member = self._properties_specs[property_name] # Get the property value. value = getattr(self._object, property_name) variant = get_variant(member.type, value) # Create a request. requests[member.interface_name][member.name] = variant return requests.items() def check_property(self, property_name): """Check if the property name is valid.""" if property_name not in self._properties_specs: raise PropertiesException( "DBus object has no property '{}'.".format( property_name ) ) def update(self, property_name): """Update the cache.""" self.check_property(property_name) self._properties_names.add(property_name) class PropertiesInterface(metaclass=ABCMeta): """Standard DBus interface org.freedesktop.DBus.Properties. DBus objects don't have to inherit this class, because the DBus library provides support for this interface by default. This class only extends this support. Report the changed property: .. code-block:: python self.report_changed_property('X') Emit all changes when the method is done: .. code-block:: python @emits_properties_changed def SetX(x: Int): self.set_x(x) """ def __init__(self): """Initialize the interface.""" self._properties_changes = PropertiesChanges(self) @dbus_signal def PropertiesChanged(self, interface: Str, changed: Dict[Str, Variant], invalid: List[Str]): """Standard signal properties changed. :param interface: a name of an interface :param changed: a dictionary of changed properties :param invalid: a list of invalidated properties :return: """ pass def report_changed_property(self, property_name): """Reports changed DBus property. :param property_name: a name of a DBus property """ self._properties_changes.update(property_name) def flush_changes(self): """Flush properties changes.""" requests = self._properties_changes.flush() for interface, changes in requests: self.PropertiesChanged(interface, changes, []) dasbus-1.7/dasbus/server/publishable.py000066400000000000000000000031261433220137200202740ustar00rootroot00000000000000# # Server support for publishable Python objects # # Copyright (C) 2019 Red Hat, Inc. All rights reserved. # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 # USA # from abc import ABCMeta, abstractmethod __all__ = ["Publishable"] class Publishable(metaclass=ABCMeta): """Abstract class for Python objects that can be published on DBus. Example: .. code-block:: python # Define a publishable class. class MyObject(Publishable): def for_publication(self): return MyDBusInterface(self) # Create a publishable object. my_object = MyObject() # Publish the object on DBus. DBus.publish_object("/org/project/x", my_object.for_publication()) """ @abstractmethod def for_publication(self): """Return a DBus representation of this object. :return: an instance of @dbus_interface or @dbus_class """ return None dasbus-1.7/dasbus/server/template.py000066400000000000000000000102401433220137200176100ustar00rootroot00000000000000# # Templates for DBus interfaces # # Copyright (C) 2019 Red Hat, Inc. All rights reserved. # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 # USA # from abc import ABCMeta from dasbus.server.property import PropertiesInterface __all__ = [ "BasicInterfaceTemplate", "InterfaceTemplate" ] class BasicInterfaceTemplate(metaclass=ABCMeta): """Basic template for a DBus interface. This template uses a software design pattern called proxy. This class provides a recommended way how to define DBus interfaces and create publishable DBus objects. The class that defines a DBus interface should inherit this class and be decorated with @dbus_class or @dbus_interface decorator. The implementation of this interface will be provided by a separate object called implementation. Therefore the methods of this class should call the methods of the implementation, the signals should be connected to the signals of the implementation and the getters and setters of properties should access the properties of the implementation. .. code-block:: python @dbus_interface("org.myproject.X") class InterfaceX(BasicInterfaceTemplate): def DoSomething(self) -> Str: return self.implementation.do_something() class X(object): def do_something(self): return "Done!" x = X() i = InterfaceX(x) DBus.publish_object("/org/myproject/X", i) """ def __init__(self, implementation): """Create a publishable DBus object. :param implementation: an implementation of this interface """ self._implementation = implementation self.connect_signals() @property def implementation(self): """Return the implementation of this interface. :return: an implementation """ return self._implementation def connect_signals(self): """Interconnect the signals. You should connect the emit methods of the interface signals to the signals of the implementation. Every time the implementation emits a signal, this interface reemits the signal on DBus. """ pass class InterfaceTemplate(BasicInterfaceTemplate, PropertiesInterface): """Template for a DBus interface. The interface provides the support for the standard interface org.freedesktop.DBus.Properties. Usage: .. code-block:: python def connect_signals(self): super().connect_signals() self.implementation.module_properties_changed.connect( self.flush_changes ) self.watch_property("X", self.implementation.x_changed) @property def X(self, x) -> Int: return self.implementation.x @emits_properties_changed def SetX(self, x: Int): self.implementation.set_x(x) """ def __init__(self, implementation): PropertiesInterface.__init__(self) BasicInterfaceTemplate.__init__(self, implementation) def watch_property(self, property_name, signal): """Watch a DBus property. Report a change when the property is changed. :param property_name: a name of a DBus property :param signal: a signal that emits when the property is changed """ self._properties_changes.check_property(property_name) def callback(*args, **kwargs): self.report_changed_property(property_name) signal.connect(callback) dasbus-1.7/dasbus/signal.py000066400000000000000000000041571433220137200157560ustar00rootroot00000000000000# # Representation of a signal # # Copyright (C) 2019 Red Hat, Inc. All rights reserved. # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 # USA # __all__ = ["Signal"] class Signal(object): """Default representation of a signal.""" __slots__ = [ "_callbacks", "__weakref__" ] def __init__(self): """Create a new signal.""" self._callbacks = [] def connect(self, callback): """Connect to a signal. :param callback: a function to register """ self._callbacks.append(callback) def __call__(self, *args, **kwargs): """Emit a signal with the given arguments.""" self.emit(*args, **kwargs) def emit(self, *args, **kwargs): """Emit a signal with the given arguments.""" # The list of callbacks can be changed, so # use a copy of the list for the iteration. for callback in self._callbacks.copy(): callback(*args, **kwargs) def disconnect(self, callback=None): """Disconnect from a signal. If no callback is specified, then all functions will be unregistered from the signal. If the specified callback isn't registered, do nothing. :param callback: a function to unregister or None """ if callback is None: self._callbacks.clear() return try: self._callbacks.remove(callback) except ValueError: pass dasbus-1.7/dasbus/specification.py000066400000000000000000000217031433220137200173150ustar00rootroot00000000000000# # Support for DBus XML specifications # # Copyright (C) 2019 Red Hat, Inc. All rights reserved. # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 # USA # # For more info about DBus specification see: # https://dbus.freedesktop.org/doc/dbus-specification.html#introspection-format # from collections import namedtuple from dasbus.xml import XMLParser __all__ = [ "DBusSpecificationError", "DBusSpecification", "DBusSpecificationParser" ] class DBusSpecificationError(Exception): """Exception for the DBus specification errors.""" pass class DBusSpecification(object): """DBus XML specification.""" DIRECTION_IN = "in" DIRECTION_OUT = "out" ACCESS_READ = "read" ACCESS_WRITE = "write" ACCESS_READWRITE = "readwrite" RETURN_PARAMETER = "return" STANDARD_INTERFACES = """ """ # Representation of specification members. Signal = namedtuple("Signal", [ "name", "interface_name", "type" ]) Method = namedtuple("Method", [ "name", "interface_name", "in_type", "out_type" ]) Property = namedtuple("Property", [ "name", "interface_name", "readable", "writable", "type" ]) # Specification data holders. __slots__ = ["_members"] @classmethod def from_xml(cls, xml): """Return a DBus specification for the given XML.""" return DBusSpecificationParser.parse_specification(xml, cls) def __init__(self): """Create a new DBus specification.""" self._members = {} @property def interfaces(self): """Interfaces of the DBus specification.""" return list(dict(self._members.keys()).keys()) @property def members(self): """Members of the DBus specification.""" return list(self._members.values()) def add_member(self, member): """Add a member of a DBus interface.""" self._members[(member.interface_name, member.name)] = member def get_member(self, interface_name, member_name): """Get a member of a DBus interface.""" try: return self._members[(interface_name, member_name)] except KeyError: pass raise DBusSpecificationError( "DBus specification has no member '{}.{}'.".format( interface_name, member_name ) ) class DBusSpecificationParser(object): """Class for parsing DBus XML specification.""" # The XML parser. xml_parser = XMLParser @classmethod def parse_specification(cls, xml, factory=DBusSpecification): """Generate a representation of a DBus XML specification. :param xml: the XML specification to parse :param factory: the DBus specification factory :return: a representation od the DBus specification """ specification = factory() cls._parse_xml(specification, DBusSpecification.STANDARD_INTERFACES) cls._parse_xml(specification, xml) return specification @classmethod def _parse_xml(cls, specification, xml): """Parse the given XML.""" node = cls.xml_parser.xml_to_element(xml) # Iterate over interfaces. for interface_element in node: if not cls.xml_parser.is_interface(interface_element): continue # Parse the interface. cls._parse_interface(specification, interface_element) @classmethod def _parse_interface(cls, specification, interface_element): """Parse the interface element from the DBus specification.""" interface_name = cls.xml_parser.get_name(interface_element) # Iterate over members. for member_element in interface_element: if cls.xml_parser.is_property(member_element): member = cls._parse_property(interface_name, member_element) elif cls.xml_parser.is_signal(member_element): member = cls._parse_signal(interface_name, member_element) elif cls.xml_parser.is_method(member_element): member = cls._parse_method(interface_name, member_element) else: continue # Add the member specification to the mapping. specification.add_member(member) return interface_name @classmethod def _parse_property(cls, interface_name, property_element): """Parse the property element from the DBus specification.""" property_name = cls.xml_parser.get_name(property_element) property_type = cls.xml_parser.get_type(property_element) property_access = cls.xml_parser.get_access(property_element) readable = property_access in ( DBusSpecification.ACCESS_READ, DBusSpecification.ACCESS_READWRITE ) writable = property_access in ( DBusSpecification.ACCESS_WRITE, DBusSpecification.ACCESS_READWRITE ) return DBusSpecification.Property( name=property_name, interface_name=interface_name, readable=readable, writable=writable, type=property_type ) @classmethod def _parse_signal(cls, interface_name, signal_element): """Parse the signal element from the DBus specification.""" signal_name = cls.xml_parser.get_name(signal_element) signal_type = [] for element in signal_element: if not cls.xml_parser.is_parameter(element): continue element_type = cls.xml_parser.get_type(element) signal_type.append(element_type) return DBusSpecification.Signal( name=signal_name, interface_name=interface_name, type=cls._get_type(signal_type) ) @classmethod def _parse_method(cls, interface_name, method_element): """Parse the method element from the DBus specification.""" method_name = cls.xml_parser.get_name(method_element) in_types = [] out_types = [] for element in method_element: if not cls.xml_parser.is_parameter(element): continue direction = cls.xml_parser.get_direction(element) element_type = cls.xml_parser.get_type(element) if direction == DBusSpecification.DIRECTION_IN: in_types.append(element_type) elif direction == DBusSpecification.DIRECTION_OUT: out_types.append(element_type) return DBusSpecification.Method( name=method_name, interface_name=interface_name, in_type=cls._get_type(in_types), out_type=cls._get_type(out_types) ) @classmethod def _get_type(cls, types): """Join types into one value.""" if not types: return None return "({})".format("".join(types)) dasbus-1.7/dasbus/structure.py000066400000000000000000000310561433220137200165370ustar00rootroot00000000000000# # Support for DBus structures # # Copyright (C) 2019 Red Hat, Inc. All rights reserved. # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 # USA # import inspect from abc import ABCMeta from typing import get_type_hints from dasbus.typing import get_variant, get_type_arguments, unwrap_variant, \ is_base_type, Structure, List __all__ = [ "DBusStructureError", "DBusData", "generate_string_from_data", "compare_data" ] # Class attribute for DBus fields. DBUS_FIELDS_ATTRIBUTE = "__dbus_fields__" class DBusStructureError(Exception): """General exception for DBus structure errors.""" pass class DBusField(object): """Description of a field in a DBus structure.""" def __init__(self, name, type_hint, description=""): """Create a description of the field. :param name: a name of the field :param type_hint: a type hint :param description: a description """ self._name = name self._type_hint = type_hint self._description = description @property def name(self): """Name of the field. :return: a name """ return self._name @property def type_hint(self): """Type hint of the field. :return: a type hint """ return self._type_hint @property def description(self): """Description of the field. :return: a description """ return self._description @property def data_name(self): """Return name of a data attribute. :return: a data attribute name """ return self.name.replace('-', '_') def set_data(self, obj, value): """Set the data attribute. :param obj: a data object :param value: a value """ setattr(obj, self.data_name, value) def set_data_variant(self, obj, variant): """Set the data attribute from a variant. :param obj: a data object :param variant: a variant """ self.set_data(obj, unwrap_variant(variant)) def get_data(self, obj): """Get the data attribute. :param obj: a data object :return: a value """ return getattr(obj, self.data_name) def get_data_variant(self, obj): """Get a variant of the data attribute. :param obj: a data object :return: a variant """ return get_variant(self.type_hint, self.get_data(obj)) class DBusDataField(DBusField): """Description of a data field in a DBus structure.""" def __init__(self, name, data_type, description=""): """Create a description of the field. :param name: a name of the field :param data_type: a subclass of DBusData :param description: a description """ super().__init__(name, Structure, description) self._data_type = data_type @property def data_type(self): """Type of the data structure. :return: a subclass of DBusData """ return self._data_type def set_data(self, obj, value): """Set the data attribute.""" value = self._data_type.from_structure(value) super().set_data(obj, value) def get_data_variant(self, obj): """Get a variant of the data attribute.""" value = self._data_type.to_structure(self.get_data(obj)) return get_variant(self._type_hint, value) class DBusDataListField(DBusField): """Description of a data list field in a DBus structure.""" def __init__(self, name, data_type, description=""): """Create a description of the field. :param name: a name of the field :param data_type: a subclass of DBusData :param description: a description """ super().__init__(name, List[Structure], description) self._data_type = data_type @property def data_type(self): """Type of the data structure. :return: a subclass of DBusData """ return self._data_type def set_data(self, obj, value): """Set the data attribute.""" value = self._data_type.from_structure_list(value) super().set_data(obj, value) def get_data_variant(self, obj): """Get a variant of the data attribute.""" value = self._data_type.to_structure_list(self.get_data(obj)) return get_variant(self._type_hint, value) class DBusData(metaclass=ABCMeta): """Object representation of data in a DBus structure. Classes derived from this class should represent specific types of DBus structures. They will support a conversion from a DBus structure of this type to a Python object and back. """ def __init_subclass__(cls, *args, **kwargs): """Create a new data class.""" super().__init_subclass__(*args, **kwargs) # Generate the DBus fields from the members of the class cls. setattr( cls, DBUS_FIELDS_ATTRIBUTE, DBusFieldFactory.generate_fields(cls) ) @classmethod def from_structure(cls, structure: Structure): """Convert a DBus structure to a data object. :param structure: a DBus structure :return: a data object """ if not isinstance(structure, dict): raise TypeError( "Invalid type '{}'.".format(type(structure).__name__) ) data = cls() fields = get_fields(cls) for name, variant in structure.items(): field = fields.get(name, None) if not field: raise DBusStructureError( "Field '{}' doesn't exist.".format(name) ) field.set_data_variant(data, variant) return data @classmethod def to_structure(cls, data) -> Structure: """Convert this data object to a DBus structure. :return: a DBus structure """ if not isinstance(data, cls): raise TypeError( "Invalid type '{}'.".format(type(data).__name__) ) structure = {} fields = get_fields(cls) for name, field in fields.items(): structure[name] = field.get_data_variant(data) return structure @classmethod def from_structure_list(cls, structures: List[Structure]): """Convert DBus structures to data objects. :param structures: a list of DBus structures :return: a list of data objects """ if not isinstance(structures, list): raise TypeError( "Invalid type '{}'.".format(type(structures).__name__) ) return list(map(cls.from_structure, structures)) @classmethod def to_structure_list(cls, objects) -> List[Structure]: """Convert data objects to DBus structures. :param objects: a list of data objects :return: a list of DBus structures """ return list(map(cls.to_structure, objects)) def __repr__(self): """Convert this data object to a string.""" return generate_string_from_data(self) def get_fields(obj): """Return DBus fields of a data object. :param obj: a data object :return: a map of DBus fields """ fields = getattr(obj, DBUS_FIELDS_ATTRIBUTE, None) if fields is None: raise DBusStructureError( "Fields are not defined at '{}'.".format(DBUS_FIELDS_ATTRIBUTE) ) return fields class DBusFieldFactory(object): """A DBus field factory.""" @classmethod def generate_fields(cls, data_class): """Generate DBus fields from properties of a class. Properties of the class will be used to generate a map of a DBus fields. The property should have a getter and a setter, otherwise an error is raised. The type hint of the getter is used to define the type of the DBus field. :param data_class: a data class :return: a map of DBus fields :raise DBusStructureError: if the DBus fields cannot be generated """ fields = {} for member_name, member in inspect.getmembers(data_class): if not cls._is_field(member_name, member): continue name = cls._get_field_name(member_name) type_hint = cls._get_member_hint(name, member) fields[name] = cls._create_field(name, type_hint) if not fields: raise DBusStructureError("No fields found.") return fields @classmethod def _is_field(cls, member_name, member): """Is the member a representation of a DBus field? :param member_name: a name of the class member :param member: a class member :return: True or False """ # Skip private members. if member_name.startswith("_"): return False # Skip all but properties. if not isinstance(member, property): return False return True @classmethod def _get_field_name(cls, member_name): """Get the name of the DBus field. :param member_name: a name of the class member :return: a name of the DBus field """ return member_name.replace('_', '-') @classmethod def _get_member_hint(cls, field_name, member): """Get the type hint of the member. :param field_name: a name of the DBus field :param member: a class member :return: a type hint """ if not member.fset: raise DBusStructureError( "Field '{}' cannot be set.".format(field_name) ) if not member.fget: raise DBusStructureError( "Field '{}' cannot be get.".format(field_name) ) getter_type_hints = get_type_hints(member.fget) type_hint = getter_type_hints.get('return', None) if not type_hint: raise DBusStructureError( "Field '{}' has unknown type.".format(field_name) ) return type_hint @classmethod def _create_field(cls, field_name, member_hint): """Create a representation of a DBus field. :param field_name: a name of the field :param member_hint: a type hint of the member :return: a new instance of DBus field """ if is_base_type(member_hint, DBusData): return DBusDataField(field_name, member_hint) if is_base_type(member_hint, List): (arg_hint, ) = get_type_arguments(member_hint) if is_base_type(arg_hint, DBusData): return DBusDataListField(field_name, arg_hint) return DBusField(field_name, member_hint) def generate_string_from_data(obj, skip=None, add=None): """Generate a string representation of a data object. Set the argument 'skip' to skip attributes with sensitive data. Set the argument 'add' to add other values to the string representation. The attributes in the string representation will be sorted alphabetically. :param obj: a data object :param skip: a list of names that should be skipped or None :param add: a dictionary of attributes to add or None :return: a string representation of the data object """ dictionary = {} for field in get_fields(obj).values(): dictionary[field.data_name] = field.get_data(obj) for name in skip or []: dictionary.pop(name, None) for name in add or {}: dictionary[name] = add[name] attributes = sorted([ "{}={}".format(name, repr(value)) for name, value in dictionary.items() ]) return "{}({})".format(obj.__class__.__name__, ", ".join(attributes)) def compare_data(obj, other): """Compare data of the given data objects. :param obj: a data object :param other: another data object :return: True if the data is equal, otherwise False """ return isinstance(obj, DBusData) \ and isinstance(other, DBusData) \ and (obj.to_structure(obj) == other.to_structure(other)) dasbus-1.7/dasbus/typing.py000066400000000000000000000276131433220137200160150ustar00rootroot00000000000000# # Support for DBus types # # Copyright (C) 2019 Red Hat, Inc. All rights reserved. # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 # USA # # For more info about DBus type system see: # https://dbus.freedesktop.org/doc/dbus-specification.html#type-system. # from typing import Tuple, Dict, List, NewType import gi gi.require_version("GLib", "2.0") from gi.repository.GLib import Variant, VariantType __all__ = [ "Bool", "Str", "Double", "Byte", "Int", "Int16", "UInt16", "Int32", "UInt32", "Int64", "UInt64", "UnixFD", "ObjPath", "Variant", "VariantType", "Tuple", "List", "Dict", "Structure", "get_native", "get_variant", "get_variant_type", "is_tuple_of_one", "unwrap_variant", "is_base_type", "get_type_arguments", "get_dbus_type", "VariantUnpacking", "VariantUnpacker", "VariantUnwrapper", ] # Basic types. Bool = bool Double = float Str = str # Default integer type: int will be treated as Int32. Int = int # All integer types. Byte = NewType('Byte', int) Int16 = NewType('Int16', int) UInt16 = NewType('UInt16', int) Int32 = NewType('Int32', int) UInt32 = NewType('UInt32', int) Int64 = NewType('Int64', int) UInt64 = NewType('UInt64', int) UnixFD = NewType('UnixFD', int) # Type of an object path. ObjPath = NewType('ObjPath', str) # Container types. # Use Tuple, Dict and List from typing. # Use Variant from GLib and get_variant. # Use Structure instead of Dict[Str, Variant]. Structure = Dict[Str, Variant] def get_dbus_type(type_hint): """Return DBus representation of a type hint. :param type_hint: a type hint :return: a string with DBus representation """ return DBusType.get_dbus_representation(type_hint) def get_variant(type_hint, value): """Return a variant data type. The type of a variant is specified with a type hint. Example: .. code-block:: python v1 = get_variant(Bool, True) v2 = get_variant(List[Int], [1,2,3]) :param type_hint: a type hint or a type string :param value: a value of the variant :return: an instance of Variant """ if type(type_hint) == str: type_string = type_hint else: type_string = get_dbus_type(type_hint) if value is None: raise TypeError("Invalid DBus value 'None'.") return Variant(type_string, value) def get_variant_type(type_hint): """Return a type of a variant data type. :param type_hint: a type hint or a type string :return: an instance of VariantType """ if type(type_hint) == str: type_string = type_hint else: type_string = get_dbus_type(type_hint) return VariantType.new(type_string) def is_tuple_of_one(type_hint): """Is the type hint a tuple of one item? :param type_hint: a type hint or a type string :return: True or False """ variant_type = get_variant_type(type_hint) return variant_type.is_tuple() and variant_type.n_items() == 1 def get_native(value): """Decompose a DBus value into a native Python object. This function is useful for testing, when the DBus library doesn't decompose arguments and return values of DBus calls. :param value: a DBus value :return: a native Python object """ if isinstance(value, Variant): return value.unpack() if isinstance(value, tuple): return tuple(map(get_native, value)) if isinstance(value, list): return list(map(get_native, value)) if isinstance(value, dict): return {k: get_native(v) for k, v in value.items()} return value def unwrap_variant(variant): """Unwrap a variant data type. Unlike the unpack method of the Variant class, this function doesn't recursively unpacks all variants in the data structure. It will unpack only the topmost variant. The implementation is inspired by the unpack method. :param variant: a variant :return: a value """ return VariantUnwrapper.apply(variant) def is_base_type(type_hint, base_type): """Is the given base type a base of the specified type hint? For example, List is a base of the type hint List[Int] and Int is a base of the type hint Int. A class is a base of itself and of every subclass of this class. :param type_hint: a type hint :param base_type: a base type :return: True or False """ type_hint = getattr(type_hint, "__origin__", type_hint) if type_hint == base_type: return True try: return issubclass(type_hint, base_type) except TypeError: pass return False def get_type_arguments(type_hint): """Get the arguments of the type hint. For example, Str and Int are arguments of the type hint Tuple(Str, Int). :param type_hint: a type hint :return: a type arguments """ return getattr(type_hint, "__args__", ()) def get_type_name(type_hint): """Get the name of the type hint. :param type_hint: a type hint :return: a name of the type hint """ return getattr(type_hint, "__name__", str(type_hint)) class DBusType(object): """Class for transforming type hints to DBus types.""" # DBus representation of basic types. _basic_type_mapping = { # Basic types. Bool: "b", Str: "s", Double: "d", # Default integer. Int: "i", # Integer types. Byte: "y", Int16: "n", UInt16: "q", Int32: "i", UInt32: "u", Int64: "x", UInt64: "t", # Other basic types. UnixFD: "h", ObjPath: "o", Variant: "v" } # DBus representation of container types. # pylint: disable=unhashable-member _container_type_mapping = { Tuple: "(%s)", List: "a%s", Dict: "a{%s}", } # pylint: enable=unhashable-member @staticmethod def get_dbus_representation(type_hint): """Return a DBus representation of the given type hint. :param type_hint: a type hint :return str: a DBus representation of the type hint :raises ValueError: for unknown types """ # Try base types. if DBusType._is_basic_type(type_hint): return DBusType._get_basic_type(type_hint) # Try container types. if DBusType._is_container_type(type_hint): return DBusType._get_container_type(type_hint) # Or raise an error. raise TypeError( "Invalid DBus type '{}'.".format( get_type_name(type_hint) ) ) @staticmethod def _is_basic_type(type_hint): """Is it a basic type?""" return type_hint in DBusType._basic_type_mapping @staticmethod def _get_basic_type(type_hint): """Return a basic type.""" return DBusType._basic_type_mapping[type_hint] @staticmethod def _is_container_type(type_hint): """Is it a container type?""" return DBusType._get_container_base_type(type_hint) is not None @staticmethod def _get_container_base_type(type_hint): """Return a container base type.""" # Return the container base type of the "origin" or None. # See: https://bugzilla.redhat.com/show_bug.cgi?id=1598574 for base_type in DBusType._container_type_mapping: if is_base_type(type_hint, base_type): return base_type return None @staticmethod def _get_container_type(type_hint): """Return a container type.""" basetype = DBusType._get_container_base_type(type_hint) # Get the arguments of the container. args = get_type_arguments(type_hint) # Check the typing. if basetype == Dict: DBusType._check_if_valid_dictionary(type_hint) # Generate string. container = DBusType._container_type_mapping[basetype] items = [DBusType.get_dbus_representation(arg) for arg in args] return container % "".join(items) @staticmethod def _check_if_valid_dictionary(type_hint): """Check the type of a dictionary. :raises ValueError: for invalid type """ key, _ = get_type_arguments(type_hint) if DBusType._is_container_type(key) or key == Variant: raise TypeError( "Invalid DBus type of dictionary key: " "'{}'".format(get_type_name(key)) ) class VariantUnpacking(object): """Set of functions of unpacking a variant. This class is doing the same as the unpack method of the Variant class, but it allows to reuse the code for other variant modifications. """ @classmethod def _process_variant(cls, variant, *extras): """Process a variant.""" type_string = variant.get_type_string() if type_string.startswith('('): return cls._handle_tuple(variant, *extras) if type_string.startswith('a{'): return cls._handle_dictionary(variant, *extras) if type_string.startswith('a'): return cls._handle_array(variant, *extras) if type_string.startswith('v'): return cls._handle_variant(variant, *extras) return cls._handle_value(variant, *extras) @classmethod def _handle_tuple(cls, variant, *extras): """Handle a tuple.""" return tuple( cls._process_variant(variant.get_child_value(i), *extras) for i in range(variant.n_children()) ) @classmethod def _handle_dictionary(cls, variant, *extras): """Handle a dictionary.""" result = {} for i in range(variant.n_children()): entry = variant.get_child_value(i) key = cls._process_variant(entry.get_child_value(0), *extras) value = cls._process_variant(entry.get_child_value(1), *extras) result[key] = value return result @classmethod def _handle_array(cls, variant, *extras): """Handle an array.""" return list( cls._process_variant(variant.get_child_value(i), *extras) for i in range(variant.n_children()) ) @classmethod def _handle_variant(cls, variant, *extras): """Handle a variant.""" return cls._process_variant(variant.get_variant(), *extras) @classmethod def _handle_value(cls, variant, *extras): """Handle a basic value.""" return variant.unpack() class VariantUnpacker(VariantUnpacking): """Class for unpacking variants.""" @classmethod def apply(cls, variant): """Unpack the specified variant. :param variant: a variant to unpack :return: an unpacked value """ return cls._process_variant(variant) class VariantUnwrapper(VariantUnpacking): """Class for unwrapping variants.""" @classmethod def apply(cls, variant): """Unwrap the specified variant. :param variant: a variant to unwrap :return: a unwrapped value """ return cls._process_variant(variant) @classmethod def _handle_variant(cls, variant, *extras): """Handle a variant. Don't recursively unpack all variants. Unpack only the topmost variant. """ return variant.get_variant() dasbus-1.7/dasbus/unix.py000066400000000000000000000211421433220137200154550ustar00rootroot00000000000000# # Support for Unix file descriptors. # # Copyright (C) 2022 Red Hat, Inc. All rights reserved. # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 # USA # import logging from dasbus.constants import DBUS_FLAG_NONE from dasbus.typing import VariantUnpacking, get_variant from dasbus.client.handler import GLibClient from dasbus.server.handler import GLibServer import gi gi.require_version("Gio", "2.0") from gi.repository import Gio log = logging.getLogger(__name__) __all__ = [ "GLibClientUnix", "GLibServerUnix", ] def acquire_fds(variant): """Acquire Unix file descriptors contained in a variant. Return a variant with indexes into a list of Unix file descriptors and the list of Unix file descriptors. If the variant is None, or the variant doesn't contain any Unix file descriptors, return None instead of the list. :param variant: a variant with Unix file descriptors :return: a variant with indexes and a list of Unix file descriptors """ if variant is None: return None, None fd_list = [] def _get_idx(fd): fd_list.append(fd) return len(fd_list) - 1 variant_without_fds = UnixFDSwap.apply(variant, _get_idx) if not fd_list: return variant, None return variant_without_fds, Gio.UnixFDList.new_from_array(fd_list) def restore_fds(variant, fd_list: Gio.UnixFDList): """Restore Unix file descriptors in a variant. If the variant is None, return None. Otherwise, return a variant with Unix file descriptors. :param variant: a variant with indexes into fd_list :param fd_list: a list of Unix file descriptors :return: a variant with Unix file descriptors """ if variant is None: return None if fd_list is None: return variant fd_list = fd_list.steal_fds() if not fd_list: return variant def _get_fd(index): try: return fd_list[index] except IndexError: return -1 return UnixFDSwap.apply(variant, _get_fd) class UnixFDSwap(VariantUnpacking): """Class for swapping values of the UnixFD type.""" @classmethod def apply(cls, variant, swap): """Swap unix file descriptors with indices. The provided function should swap a unix file descriptor with an index into an array of unix file descriptors or vice versa. :param variant: a variant to modify :param swap: a swapping function :return: a modified variant """ return cls._recreate_variant(variant, swap) @classmethod def _handle_variant(cls, variant, *extras): """Handle a variant.""" return cls._recreate_variant(variant.get_variant(), *extras) @classmethod def _handle_value(cls, variant, *extras): """Handle a basic value.""" type_string = variant.get_type_string() # Handle the unix file descriptor. if type_string == 'h': # Get the swapping function. swap, *_ = extras # Swap the values. return swap(variant.get_handle()) return variant.unpack() @classmethod def _recreate_variant(cls, variant, *extras): """Create a variant with swapped values.""" type_string = variant.get_type_string() # Do nothing if there is no unix file descriptor to handle. if 'h' not in type_string and 'v' not in type_string: return variant # Get a new value of the variant. value = cls._process_variant(variant, *extras) # Create a new variant. return get_variant(type_string, value) class GLibClientUnix(GLibClient): """The low-level DBus client library based on GLib.""" @classmethod def sync_call(cls, connection, service_name, object_path, interface_name, method_name, parameters, reply_type, flags=DBUS_FLAG_NONE, timeout=GLibClient.DBUS_TIMEOUT_NONE): """Synchronously call a DBus method. :return: a result of the DBus call """ # Process Unix file descriptors in parameters. parameters, fd_list = acquire_fds(parameters) # Call the DBus method. result = connection.call_with_unix_fd_list_sync( service_name, object_path, interface_name, method_name, parameters, reply_type, flags, timeout, fd_list, None ) # Restore Unix file descriptors in the result. return restore_fds(*result) @classmethod def async_call(cls, connection, service_name, object_path, interface_name, method_name, parameters, reply_type, callback, callback_args=(), flags=DBUS_FLAG_NONE, timeout=GLibClient.DBUS_TIMEOUT_NONE): """Asynchronously call a DBus method.""" # Process Unix file descriptors in parameters. parameters, fd_list = acquire_fds(parameters) # Call the DBus method. connection.call_with_unix_fd_list( service_name, object_path, interface_name, method_name, parameters, reply_type, flags, timeout, fd_list, callback=cls._async_call_finish, user_data=(callback, callback_args) ) @classmethod def _async_call_finish(cls, source_object, result_object, user_data): """Finish an asynchronous DBus method call.""" # Prepare the user's callback. callback, callback_args = user_data def _finish_call(): # Retrieve the result of the call. result = source_object.call_with_unix_fd_list_finish( result_object ) # Restore Unix file descriptors in the result. return restore_fds(*result) # Call user's callback. callback(_finish_call, *callback_args) class GLibServerUnix(GLibServer): """The low-level DBus server library based on GLib. Adds Unix FD Support to base class""" @classmethod def emit_signal(cls, connection, object_path, interface_name, signal_name, parameters, destination=None): """Emit a DBus signal. GLib doesn't seem to support Unix file descriptors in signals. Swap Unix file descriptors with indexes into a list of Unix file descriptors, but emit just the indexes. Log a warning to inform users about the limited support. """ # Process Unix file descriptors in parameters. parameters, fd_list = acquire_fds(parameters) if fd_list: log.warning("Unix file descriptors in signals are unsupported.") # Emit the signal without Unix file descriptors. connection.emit_signal( destination, object_path, interface_name, signal_name, parameters ) @classmethod def set_call_reply(cls, invocation, out_type, out_value): """Set the reply of the DBus call.""" # Process Unix file descriptors in the reply. reply_value = cls._get_reply_value(out_type, out_value) reply_args = acquire_fds(reply_value) # Send the reply. invocation.return_value_with_unix_fd_list(*reply_args) @classmethod def _object_callback(cls, connection, sender, object_path, interface_name, method_name, parameters, invocation, user_data): """A method call closure of a DBus object.""" # Prepare the user's callback. callback, callback_args = user_data # Restore Unix file descriptors in parameters. fd_list = invocation.get_message().get_unix_fd_list() parameters = restore_fds(parameters, fd_list) # Call user's callback. callback( invocation, interface_name, method_name, parameters, *callback_args ) dasbus-1.7/dasbus/xml.py000066400000000000000000000116221433220137200152740ustar00rootroot00000000000000# # Support for XML representation # # Copyright (C) 2019 Red Hat, Inc. All rights reserved. # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 # USA # # For more info about DBus specification see: # https://dbus.freedesktop.org/doc/dbus-specification.html#introspection-format # from xml.etree import ElementTree from xml.dom import minidom __all__ = [ "XMLParser", "XMLGenerator" ] class XMLParser(object): """Class for parsing XML.""" @staticmethod def xml_to_element(xml): return ElementTree.fromstring(xml) @staticmethod def is_member(member_node): return member_node.tag in ("method", "signal", "property") @staticmethod def is_interface(member_node): return member_node.tag == "interface" @staticmethod def is_signal(member_node): return member_node.tag == "signal" @staticmethod def is_method(member_node): return member_node.tag == "method" @staticmethod def is_property(member_node): return member_node.tag == "property" @staticmethod def is_parameter(member_node): return member_node.tag == "arg" @staticmethod def has_name(node, node_name): return node.attrib.get("name", "") == node_name @staticmethod def get_name(node): return node.attrib["name"] @staticmethod def get_type(node): return node.attrib["type"] @staticmethod def get_access(node): return node.attrib["access"] @staticmethod def get_direction(node): return node.attrib["direction"] @staticmethod def get_interfaces_from_node(node_element): """Return a dictionary of interfaces defined in a node element.""" interfaces = {} for element in node_element.iterfind("interface"): interfaces[element.attrib["name"]] = element return interfaces class XMLGenerator(XMLParser): """Class for generating XML.""" @staticmethod def element_to_xml(element): """Return XML of the element.""" return ElementTree.tostring( element, method="xml", encoding="unicode" ) @staticmethod def prettify_xml(xml): """Return pretty printed normalized XML. Python 3.8 changed the order of the attributes and introduced the function canonicalize that should be used to normalize XML. """ # Remove newlines and extra whitespaces, xml_line = "".join([line.strip() for line in xml.splitlines()]) # Generate pretty xml. xml = minidom.parseString(xml_line).toprettyxml(indent=" ") # Normalize attributes. canonicalize = getattr( ElementTree, "canonicalize", lambda xml, *args, **kwargs: xml ) return canonicalize(xml, with_comments=True) @staticmethod def add_child(parent_element, child_element): """Append the child element to the parent element.""" parent_element.append(child_element) @staticmethod def add_comment(element, comment): element.append(ElementTree.Comment(text=comment)) @staticmethod def create_node(): """Create a node element called node.""" return ElementTree.Element("node") @staticmethod def create_interface(name): """Create an interface element.""" return ElementTree.Element("interface", {"name": name}) @staticmethod def create_signal(name): """Create a signal element.""" return ElementTree.Element("signal", {"name": name}) @staticmethod def create_method(name): """Create a method element.""" return ElementTree.Element("method", {"name": name}) @staticmethod def create_parameter(name, param_type, direction): """Create a parameter element.""" tag = "arg" attr = { "name": name, "type": param_type, "direction": direction } return ElementTree.Element(tag, attr) @staticmethod def create_property(name, property_type, access): """Create a property element.""" tag = "property" attr = { "name": name, "type": property_type, "access": access } return ElementTree.Element(tag, attr) dasbus-1.7/docs/000077500000000000000000000000001433220137200135675ustar00rootroot00000000000000dasbus-1.7/docs/Makefile000066400000000000000000000013431433220137200152300ustar00rootroot00000000000000# Minimal makefile for Sphinx documentation # # You can set these variables from the command line, and also # from the environment for the first two. SPHINXOPTS ?= -W --keep-going SPHINXBUILD ?= sphinx-build SOURCEDIR = . BUILDDIR = _build # Put it first so that "make" without argument is like "make help". help: @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) # Generate the API documentation. api: sphinx-apidoc-3 --separate -o api ../dasbus .PHONY: help api Makefile # Catch-all target: route all unknown targets to Sphinx using the new # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). %: Makefile @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) dasbus-1.7/docs/api/000077500000000000000000000000001433220137200143405ustar00rootroot00000000000000dasbus-1.7/docs/api/dasbus.client.handler.rst000066400000000000000000000002271433220137200212450ustar00rootroot00000000000000dasbus.client.handler module ============================ .. automodule:: dasbus.client.handler :members: :undoc-members: :show-inheritance: dasbus-1.7/docs/api/dasbus.client.observer.rst000066400000000000000000000002321433220137200214530ustar00rootroot00000000000000dasbus.client.observer module ============================= .. automodule:: dasbus.client.observer :members: :undoc-members: :show-inheritance: dasbus-1.7/docs/api/dasbus.client.property.rst000066400000000000000000000002321433220137200215100ustar00rootroot00000000000000dasbus.client.property module ============================= .. automodule:: dasbus.client.property :members: :undoc-members: :show-inheritance: dasbus-1.7/docs/api/dasbus.client.proxy.rst000066400000000000000000000002211433220137200210030ustar00rootroot00000000000000dasbus.client.proxy module ========================== .. automodule:: dasbus.client.proxy :members: :undoc-members: :show-inheritance: dasbus-1.7/docs/api/dasbus.client.rst000066400000000000000000000002371433220137200176320ustar00rootroot00000000000000dasbus.client package ===================== .. toctree:: dasbus.client.handler dasbus.client.observer dasbus.client.property dasbus.client.proxy dasbus-1.7/docs/api/dasbus.connection.rst000066400000000000000000000002131433220137200205050ustar00rootroot00000000000000dasbus.connection module ======================== .. automodule:: dasbus.connection :members: :undoc-members: :show-inheritance: dasbus-1.7/docs/api/dasbus.constants.rst000066400000000000000000000002101433220137200203570ustar00rootroot00000000000000dasbus.constants module ======================= .. automodule:: dasbus.constants :members: :undoc-members: :show-inheritance: dasbus-1.7/docs/api/dasbus.error.rst000066400000000000000000000001741433220137200175050ustar00rootroot00000000000000dasbus.error module =================== .. automodule:: dasbus.error :members: :undoc-members: :show-inheritance: dasbus-1.7/docs/api/dasbus.identifier.rst000066400000000000000000000002131433220137200204700ustar00rootroot00000000000000dasbus.identifier module ======================== .. automodule:: dasbus.identifier :members: :undoc-members: :show-inheritance: dasbus-1.7/docs/api/dasbus.loop.rst000066400000000000000000000001711433220137200173220ustar00rootroot00000000000000dasbus.loop module ================== .. automodule:: dasbus.loop :members: :undoc-members: :show-inheritance: dasbus-1.7/docs/api/dasbus.namespace.rst000066400000000000000000000002101433220137200202770ustar00rootroot00000000000000dasbus.namespace module ======================= .. automodule:: dasbus.namespace :members: :undoc-members: :show-inheritance: dasbus-1.7/docs/api/dasbus.rst000066400000000000000000000004731433220137200163570ustar00rootroot00000000000000Public API ========== .. toctree:: :maxdepth: 1 :caption: Contents: dasbus.client dasbus.server dasbus.connection dasbus.constants dasbus.error dasbus.identifier dasbus.loop dasbus.namespace dasbus.signal dasbus.specification dasbus.structure dasbus.typing dasbus.xml dasbus-1.7/docs/api/dasbus.server.container.rst000066400000000000000000000002351433220137200216410ustar00rootroot00000000000000dasbus.server.container module ============================== .. automodule:: dasbus.server.container :members: :undoc-members: :show-inheritance: dasbus-1.7/docs/api/dasbus.server.handler.rst000066400000000000000000000002271433220137200212750ustar00rootroot00000000000000dasbus.server.handler module ============================ .. automodule:: dasbus.server.handler :members: :undoc-members: :show-inheritance: dasbus-1.7/docs/api/dasbus.server.interface.rst000066400000000000000000000002351433220137200216170ustar00rootroot00000000000000dasbus.server.interface module ============================== .. automodule:: dasbus.server.interface :members: :undoc-members: :show-inheritance: dasbus-1.7/docs/api/dasbus.server.property.rst000066400000000000000000000002321433220137200215400ustar00rootroot00000000000000dasbus.server.property module ============================= .. automodule:: dasbus.server.property :members: :undoc-members: :show-inheritance: dasbus-1.7/docs/api/dasbus.server.publishable.rst000066400000000000000000000002431433220137200221500ustar00rootroot00000000000000dasbus.server.publishable module ================================ .. automodule:: dasbus.server.publishable :members: :undoc-members: :show-inheritance: dasbus-1.7/docs/api/dasbus.server.rst000066400000000000000000000003331433220137200176570ustar00rootroot00000000000000dasbus.server package ===================== .. toctree:: dasbus.server.container dasbus.server.handler dasbus.server.interface dasbus.server.property dasbus.server.publishable dasbus.server.template dasbus-1.7/docs/api/dasbus.server.template.rst000066400000000000000000000002321433220137200214670ustar00rootroot00000000000000dasbus.server.template module ============================= .. automodule:: dasbus.server.template :members: :undoc-members: :show-inheritance: dasbus-1.7/docs/api/dasbus.signal.rst000066400000000000000000000001771433220137200176340ustar00rootroot00000000000000dasbus.signal module ==================== .. automodule:: dasbus.signal :members: :undoc-members: :show-inheritance: dasbus-1.7/docs/api/dasbus.specification.rst000066400000000000000000000002241433220137200211700ustar00rootroot00000000000000dasbus.specification module =========================== .. automodule:: dasbus.specification :members: :undoc-members: :show-inheritance: dasbus-1.7/docs/api/dasbus.structure.rst000066400000000000000000000002101433220137200204030ustar00rootroot00000000000000dasbus.structure module ======================= .. automodule:: dasbus.structure :members: :undoc-members: :show-inheritance: dasbus-1.7/docs/api/dasbus.typing.rst000066400000000000000000000001771433220137200176710ustar00rootroot00000000000000dasbus.typing module ==================== .. automodule:: dasbus.typing :members: :undoc-members: :show-inheritance: dasbus-1.7/docs/api/dasbus.xml.rst000066400000000000000000000001661433220137200171550ustar00rootroot00000000000000dasbus.xml module ================= .. automodule:: dasbus.xml :members: :undoc-members: :show-inheritance: dasbus-1.7/docs/conf.py000066400000000000000000000042451433220137200150730ustar00rootroot00000000000000#!/usr/bin/env python3 # Configuration file for the Sphinx documentation builder. # # This file only contains a selection of the most common options. For a full # list see the documentation: # http://www.sphinx-doc.org/en/master/config # -- Path setup -------------------------------------------------------------- # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # import os import sys import sphinx_rtd_theme sys.path.insert(0, os.path.abspath('..')) # -- Project information ----------------------------------------------------- project = 'dasbus' copyright = '2020, Vendula Poncova' author = 'Vendula Poncova' # -- General configuration --------------------------------------------------- # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.autosummary', 'sphinx.ext.doctest', 'sphinx.ext.intersphinx', 'sphinx.ext.todo', 'sphinx.ext.coverage', 'sphinx.ext.viewcode', 'sphinx_rtd_theme', ] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This pattern also affects html_static_path and html_extra_path. exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] # A boolean that decides whether module names are prepended to all object names. add_module_names = False # -- Options for HTML output ------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # html_theme = 'sphinx_rtd_theme' # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". # html_static_path = ['_static'] dasbus-1.7/docs/development.rst000066400000000000000000000005511433220137200166440ustar00rootroot00000000000000Development and testing ======================= Install `podman `_ and use the following command to run all tests in a container. It doesn't require any additional dependencies: .. code-block:: shell make container-ci Use the command below to run only the unit tests: .. code-block:: shell make container-ci CI_CMD="make test" dasbus-1.7/docs/environment.yml000066400000000000000000000002111433220137200166500ustar00rootroot00000000000000# Conda environment file name: dasbus-docs-environment channels: - defaults - conda-forge dependencies: - python=3 - pygobject dasbus-1.7/docs/examples.rst000066400000000000000000000217771433220137200161550ustar00rootroot00000000000000Examples ======== Look at the `complete examples `_ or `DBus services `_ of the Anaconda Installer for more inspiration. Basic usage ----------- Show the current hostname. .. code-block:: python from dasbus.connection import SystemMessageBus bus = SystemMessageBus() proxy = bus.get_proxy( "org.freedesktop.hostname1", "/org/freedesktop/hostname1" ) print(proxy.Hostname) Send a notification to the notification server. .. code-block:: python from dasbus.connection import SessionMessageBus bus = SessionMessageBus() proxy = bus.get_proxy( "org.freedesktop.Notifications", "/org/freedesktop/Notifications" ) id = proxy.Notify( "", 0, "face-smile", "Hello World!", "This notification can be ignored.", [], {}, 0 ) print("The notification {} was sent.".format(id)) Handle a closed notification. .. code-block:: python from dasbus.loop import EventLoop loop = EventLoop() from dasbus.connection import SessionMessageBus bus = SessionMessageBus() proxy = bus.get_proxy( "org.freedesktop.Notifications", "/org/freedesktop/Notifications" ) def callback(id, reason): print("The notification {} was closed.".format(id)) proxy.NotificationClosed.connect(callback) loop.run() Asynchronously fetch a list of network devices. .. code-block:: python from dasbus.loop import EventLoop loop = EventLoop() from dasbus.connection import SystemMessageBus bus = SystemMessageBus() proxy = bus.get_proxy( "org.freedesktop.NetworkManager", "/org/freedesktop/NetworkManager" ) def callback(call): print(call()) proxy.GetDevices(callback=callback) loop.run() Define the org.example.HelloWorld service. .. code-block:: python class HelloWorld(object): __dbus_xml__ = """ """ def Hello(self, name): return "Hello {}!".format(name) Define the org.example.HelloWorld service with an automatically generated XML specification. .. code-block:: python from dasbus.server.interface import dbus_interface from dasbus.typing import Str @dbus_interface("org.example.HelloWorld") class HelloWorld(object): def Hello(self, name: Str) -> Str: return "Hello {}!".format(name) print(HelloWorld.__dbus_xml__) Publish the org.example.HelloWorld service on the session message bus. .. code-block:: python from dasbus.connection import SessionMessageBus bus = SessionMessageBus() bus.publish_object("/org/example/HelloWorld", HelloWorld()) bus.register_service("org.example.HelloWorld") from dasbus.loop import EventLoop loop = EventLoop() loop.run() Support for Unix file descriptors --------------------------------- The support for Unix file descriptors is disabled by default. It needs to be explicitly enabled when you create a DBus proxy or publish a DBus object that could send or receive Unix file descriptors. .. warning:: This functionality is supported only on UNIX. Send and receive Unix file descriptors with a DBus proxy. .. code-block:: python import os from dasbus.connection import SystemMessageBus from dasbus.unix import GLibClientUnix bus = SystemMessageBus() proxy = bus.get_proxy( "org.freedesktop.login1", "/org/freedesktop/login1", client=GLibClientUnix ) fd = proxy.Inhibit( "sleep", "my-example", "Running an example", "block" ) proxy.ListInhibitors() os.close(fd) Allow to send and receive Unix file descriptors within the /org/example/HelloWorld DBus object. .. code-block:: python from dasbus.unix import GLibServerUnix bus.publish_object( "/org/example/HelloWorld", HelloWorld(), server=GLibServerUnix ) Management of DBus names and paths ---------------------------------- Use constants to define DBus services and objects. .. code-block:: python from dasbus.connection import SystemMessageBus from dasbus.identifier import DBusServiceIdentifier, DBusObjectIdentifier NETWORK_MANAGER_NAMESPACE = ( "org", "freedesktop", "NetworkManager" ) NETWORK_MANAGER = DBusServiceIdentifier( namespace=NETWORK_MANAGER_NAMESPACE, message_bus=SystemMessageBus() ) NETWORK_MANAGER_SETTINGS = DBusObjectIdentifier( namespace=NETWORK_MANAGER_NAMESPACE, basename="Settings" ) Create a proxy of the org.freedesktop.NetworkManager service. .. code-block:: python proxy = NETWORK_MANAGER.get_proxy() print(proxy.NetworkingEnabled) Create a proxy of the /org/freedesktop/NetworkManager/Settings object. .. code-block:: python proxy = NETWORK_MANAGER.get_proxy(NETWORK_MANAGER_SETTINGS) print(proxy.Hostname) See `a complete example `__. Error handling -------------- Use exceptions to propagate and handle DBus errors. Create an error mapper and a decorator for mapping Python exception classes to DBus error names. .. code-block:: python from dasbus.error import ErrorMapper, DBusError, get_error_decorator error_mapper = ErrorMapper() dbus_error = get_error_decorator(error_mapper) Use the decorator to register Python exceptions that represent DBus errors. These exceptions can be raised by DBus services and caught by DBus clients in the try-except block. .. code-block:: python @dbus_error("org.freedesktop.DBus.Error.InvalidArgs") class InvalidArgs(DBusError): pass The message bus will use the specified error mapper to automatically transform Python exceptions to DBus errors and back. .. code-block:: python from dasbus.connection import SessionMessageBus bus = SessionMessageBus(error_mapper=error_mapper) See `a complete example `__. Timeout for a DBus call ----------------------- Call DBus methods with a timeout (specified in milliseconds). .. code-block:: python proxy = NETWORK_MANAGER.get_proxy() try: proxy.CheckConnectivity(timeout=3) except TimeoutError: print("The call timed out!") Support for DBus structures --------------------------- Represent DBus structures by Python objects. A DBus structure is a dictionary of attributes that maps attribute names to variants with attribute values. Use Python objects to define such structures. They can be easily converted to a dictionary, send via DBus and converted back to an object. .. code-block:: python from dasbus.structure import DBusData from dasbus.typing import Str, get_variant class UserData(DBusData): def __init__(self): self._name = "" @property def name(self) -> Str: return self._name @name.setter def name(self, name): self._name = name data = UserData() data.name = "Alice" print(UserData.to_structure(data)) print(UserData.from_structure({ "name": get_variant(Str, "Bob") })) See `a complete example `__. Management of dynamic DBus objects ---------------------------------- Create Python objects that can be automatically published on DBus. These objects are usually managed by DBus containers and published on demand. .. code-block:: python from dasbus.server.interface import dbus_interface from dasbus.server.template import InterfaceTemplate from dasbus.server.publishable import Publishable from dasbus.typing import Str @dbus_interface("org.example.Chat") class ChatInterface(InterfaceTemplate): def Send(self, message: Str): return self.implementation.send() class Chat(Publishable): def for_publication(self): return ChatInterface(self) def send(self, message): print(message) Use DBus containers to automatically publish dynamically created Python objects. A DBus container converts publishable Python objects into DBus paths and back. It generates unique DBus paths in the specified namespace and assigns them to objects. Each object is published when its DBus path is requested for the first time. .. code-block:: python from dasbus.connection import SessionMessageBus from dasbus.server.container import DBusContainer container = DBusContainer( namespace=("org", "example", "Chat"), message_bus=SessionMessageBus() ) print(container.to_object_path(Chat())) See `a complete example `__. dasbus-1.7/docs/index.rst000066400000000000000000000047161433220137200154400ustar00rootroot00000000000000Welcome to dasbus's documentation! ================================== Dasbus is a DBus library written in Python 3, based on GLib and inspired by pydbus. The code used to be part of the `Anaconda Installer `_ project. It was based on the `pydbus `_ library, but we replaced it with our own solution because its upstream development stalled. The dasbus library is a result of this effort. Requirements ------------ - Python 3.6+ - PyGObject 3 You can install `PyGObject `_ provided by your operating system or use PyPI. The system package is usually called ``python3-gi``, ``python3-gobject`` or ``pygobject3``. See the `instructions `_ for your platform (only for PyGObject, you don't need cairo or GTK). The library is known to work with Python 3.8, PyGObject 3.34 and GLib 2.63, but these are not the required minimal versions. Installation ------------ Install the package from `PyPI `_ or install the package provided by your operating system if available. Install from PyPI ^^^^^^^^^^^^^^^^^ Follow the instructions above to install the requirements before you install ``dasbus`` with ``pip``. The required dependencies has to be installed manually in this case. :: pip3 install dasbus Install on Arch Linux ^^^^^^^^^^^^^^^^^^^^^ Build and install the community package from the `Arch User Repository `_. Follow the `guidelines `_. :: git clone https://aur.archlinux.org/python-dasbus.git cd python-dasbus makepkg -si Install on Debian / Ubuntu ^^^^^^^^^^^^^^^^^^^^^^^^^^ Install the system package on Debian 11+ or Ubuntu 22.04+. :: sudo apt install python3-dasbus Install on Fedora / CentOS / RHEL ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Install the system package on Fedora 31+, CentOS Stream 8+ or RHEL 8+. :: sudo dnf install python3-dasbus Install on openSUSE ^^^^^^^^^^^^^^^^^^^ Install the system package on openSUSE Tumbleweed or openSUSE Leap 15.2+. :: sudo zypper install python3-dasbus .. toctree:: :maxdepth: 1 :caption: Contents: Development and testing Dasbus vs pydbus Public API Examples Indices and tables ------------------ - :ref:`genindex` - :ref:`modindex` - :ref:`search` dasbus-1.7/docs/make.bat000066400000000000000000000014331433220137200151750ustar00rootroot00000000000000@ECHO OFF pushd %~dp0 REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set SOURCEDIR=. set BUILDDIR=_build if "%1" == "" goto help %SPHINXBUILD% >NUL 2>NUL if errorlevel 9009 ( echo. echo.The 'sphinx-build' command was not found. Make sure you have Sphinx echo.installed, then set the SPHINXBUILD environment variable to point echo.to the full path of the 'sphinx-build' executable. Alternatively you echo.may add the Sphinx directory to PATH. echo. echo.If you don't have Sphinx installed, grab it from echo.http://sphinx-doc.org/ exit /b 1 ) %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% goto end :help %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% :end popd dasbus-1.7/docs/pydbus.rst000066400000000000000000000105201433220137200156250ustar00rootroot00000000000000dasbus vs pydbus ================ The dasbus library used to be based on `pydbus `_, but it was later reimplemented. We have changed the API and the implementation of the library based on our experience with pydbus. However, it should be possible to modify dasbus classes to work the same way as pydbus classes. What is new ----------- - Support for asynchronous DBus calls: DBus methods can be called asynchronously. - Support for Unix file descriptors: It is possible to send or receive Unix file descriptors. - Mapping DBus errors to exceptions: Use Python exceptions to propagate and handle DBus errors. Define your own rules for mapping errors to exceptions and back. See the :class:`ErrorMapper ` class - Support for type hints: Use Python type hints from :mod:`dasbus.typing` to define DBus types. - Generating XML specifications: Automatically generate XML specifications from Python classes with the :func:`dbus_interface ` decorator. - Support for DBus structures: Represent DBus structures (dictionaries of variants) by Python objects. See the :class:`DBusData ` class. - Support for groups of DBus objects: Use DBus containers from :mod:`dasbus.server.container` to publish groups of Python objects. - Composition over inheritance: The library follows the principle of composition over inheritance. It allows to easily change the default behaviour. - Lazy DBus connections: DBus connections are established on demand. - Lazy DBus proxies: Attributes of DBus proxies are created on demand. What is different ----------------- - No context managers: There are no context managers in dasbus. Context managers and event loops don't work very well together. - No auto-completion: There is no support for automatic completion of DBus names and paths. We recommend to work with constants defined by classes from :mod:`dasbus.identifier` instead of strings. - No unpacking of variants: The dasbus library doesn't unpack variants by default. It means that values received from DBus match the types declared in the XML specification. Use the :func:`get_native ` function to unpack the values. - Obtaining proxy objects: Call the :meth:`get_proxy ` method to get a proxy of the specified DBus object. - No single-interface view: DBus proxies don't support single-interface views. Use the :class:`InterfaceProxy ` class to access a specific interface of a DBus object. - Higher priority of standard interfaces: If there is a DBus interface in the XML specification that redefines a member of a standard interface, the DBus proxy will choose a member of the standard interface. Use the :class:`InterfaceProxy ` class to access a specific interface of a DBus object. - No support for help: Members of DBus proxies are created lazily, so the build-in ``help`` function doesn't return useful information about the DBus interfaces. - Watching DBus names: Use :class:`a service observer ` to watch a DBus name. - Acquiring DBus names: Call the :meth:`register_service ` method to acquire a DBus name. - Providing XML specifications: Use the ``__dbus_xml__`` attribute to provide the XML specification of a DBus object. Or you can generate it from the code using the :func:`dbus_interface ` decorator. - No support for polkit: There is no support for the DBus service ``org.freedesktop.PolicyKit1``. What is the same (for now) -------------------------- - No support for other event loops: Dasbus uses GLib as its backend, so it requires to use the GLib event loop. However, the GLib part of dasbus is separated from the rest of the code, so it shouldn't be too difficult to add support for a different backend. It would be necessary to replace :class:`dasbus.typing.Variant` and :class:`dasbus.typing.VariantType` with their abstractions and reorganize the code. - No support for org.freedesktop.DBus.ObjectManager: There is no support for object managers, however the :class:`DBus containers ` could be a good starting point. dasbus-1.7/examples/000077500000000000000000000000001433220137200144555ustar00rootroot00000000000000dasbus-1.7/examples/01_hostname/000077500000000000000000000000001433220137200165735ustar00rootroot00000000000000dasbus-1.7/examples/01_hostname/client.py000066400000000000000000000007641433220137200204320ustar00rootroot00000000000000# # Show the current hostname. # from dasbus.connection import SystemMessageBus if __name__ == "__main__": # Create a representation of a system bus connection. bus = SystemMessageBus() # Create a proxy of the object /org/freedesktop/hostname1 # provided by the service org.freedesktop.hostname1. proxy = bus.get_proxy( "org.freedesktop.hostname1", "/org/freedesktop/hostname1" ) # Print a value of the DBus property Hostname. print(proxy.Hostname) dasbus-1.7/examples/02_notification/000077500000000000000000000000001433220137200174445ustar00rootroot00000000000000dasbus-1.7/examples/02_notification/client.py000066400000000000000000000011001433220137200212640ustar00rootroot00000000000000# # Send a notification to the notification server. # from common import NOTIFICATIONS if __name__ == "__main__": # Create a proxy of the object /org/freedesktop/Notifications # provided by the service org.freedesktop.Notifications. proxy = NOTIFICATIONS.get_proxy() # Call the DBus method Notify. notification_id = proxy.Notify( "", 0, "face-smile", "Hello World!", "This notification can be ignored.", [], {}, 0 ) # Print the return value of the call. print("The notification {} was sent.".format(notification_id)) dasbus-1.7/examples/02_notification/common.py000066400000000000000000000005321433220137200213060ustar00rootroot00000000000000# # The common definitions # from dasbus.connection import SessionMessageBus from dasbus.identifier import DBusServiceIdentifier # Define the message bus. SESSION_BUS = SessionMessageBus() # Define services and objects. NOTIFICATIONS = DBusServiceIdentifier( namespace=("org", "freedesktop", "Notifications"), message_bus=SESSION_BUS ) dasbus-1.7/examples/02_notification/listener.py000066400000000000000000000013111433220137200216370ustar00rootroot00000000000000# # Handle a closed notification. # Start the listener, run the client and close a notification. # from dasbus.loop import EventLoop from common import NOTIFICATIONS def callback(notification_id, reason): """The callback of the DBus signal NotificationClosed.""" print("The notification {} was closed.".format(notification_id)) if __name__ == "__main__": # Create a proxy of the object /org/freedesktop/Notifications # provided by the service org.freedesktop.Notifications. proxy = NOTIFICATIONS.get_proxy() # Connect the callback to the DBus signal NotificationClosed. proxy.NotificationClosed.connect(callback) # Start the event loop. loop = EventLoop() loop.run() dasbus-1.7/examples/03_helloworld/000077500000000000000000000000001433220137200171325ustar00rootroot00000000000000dasbus-1.7/examples/03_helloworld/client.py000066400000000000000000000005621433220137200207650ustar00rootroot00000000000000# # Say hello to the world. # Start the server and run the client. # from common import HELLO_WORLD if __name__ == "__main__": # Create a proxy of the object /org/example/HelloWorld # provided by the service org.example.HelloWorld proxy = HELLO_WORLD.get_proxy() # Call the DBus method Hello and print the return value. print(proxy.Hello("World")) dasbus-1.7/examples/03_helloworld/common.py000066400000000000000000000005211433220137200207720ustar00rootroot00000000000000# # The common definitions # from dasbus.connection import SessionMessageBus from dasbus.identifier import DBusServiceIdentifier # Define the message bus. SESSION_BUS = SessionMessageBus() # Define services and objects. HELLO_WORLD = DBusServiceIdentifier( namespace=("org", "example", "HelloWorld"), message_bus=SESSION_BUS ) dasbus-1.7/examples/03_helloworld/server.py000066400000000000000000000023051433220137200210120ustar00rootroot00000000000000# # Run the service org.example.HelloWorld. # from dasbus.loop import EventLoop from dasbus.server.interface import dbus_interface from dasbus.typing import Str from common import HELLO_WORLD, SESSION_BUS from dasbus.xml import XMLGenerator @dbus_interface(HELLO_WORLD.interface_name) class HelloWorld(object): """The DBus interface for HelloWorld.""" def Hello(self, name: Str) -> Str: """Generate a greeting. :param name: someone to say hello :return: a greeting """ return "Hello {}!".format(name) if __name__ == "__main__": # Print the generated XML specification. print(XMLGenerator.prettify_xml(HelloWorld.__dbus_xml__)) try: # Create an instance of the class HelloWorld. hello_world = HelloWorld() # Publish the instance at /org/example/HelloWorld. SESSION_BUS.publish_object(HELLO_WORLD.object_path, hello_world) # Register the service name org.example.HelloWorld. SESSION_BUS.register_service(HELLO_WORLD.service_name) # Start the event loop. loop = EventLoop() loop.run() finally: # Unregister the DBus service and objects. SESSION_BUS.disconnect() dasbus-1.7/examples/04_register/000077500000000000000000000000001433220137200166045ustar00rootroot00000000000000dasbus-1.7/examples/04_register/client.py000066400000000000000000000015171433220137200204400ustar00rootroot00000000000000# # Register the user Alice. # Start the server and run the client twice. # from common import REGISTER, User, InvalidUser if __name__ == "__main__": # Create a proxy of the object /org/example/Register # provided by the service org.example.Register proxy = REGISTER.get_proxy() # Register Alice. alice = User() alice.name = "Alice" alice.age = 1000 print("Sending a DBus structure:") print(User.to_structure(alice)) try: proxy.RegisterUser(User.to_structure(alice)) except InvalidUser as e: print("Failed to register a user:", e) exit(1) # Print the registered users. print("Receiving DBus structures:") for user in proxy.Users: print(user) print("Registered users:") for user in User.from_structure_list(proxy.Users): print(user.name) dasbus-1.7/examples/04_register/common.py000066400000000000000000000025011433220137200204440ustar00rootroot00000000000000# # The common definitions # from dasbus.connection import SessionMessageBus from dasbus.error import DBusError, ErrorMapper, get_error_decorator from dasbus.identifier import DBusServiceIdentifier from dasbus.structure import DBusData from dasbus.typing import Str, Int # Define the error mapper. ERROR_MAPPER = ErrorMapper() # Define the message bus. SESSION_BUS = SessionMessageBus( error_mapper=ERROR_MAPPER ) # Define namespaces. REGISTER_NAMESPACE = ("org", "example", "Register") # Define services and objects. REGISTER = DBusServiceIdentifier( namespace=REGISTER_NAMESPACE, message_bus=SESSION_BUS ) # The decorator for DBus errors. dbus_error = get_error_decorator(ERROR_MAPPER) # Define errors. @dbus_error("InvalidUserError", namespace=REGISTER_NAMESPACE) class InvalidUser(DBusError): """The user is invalid.""" pass # Define structures. class User(DBusData): """The user data.""" def __init__(self): self._name = "" self._age = 0 @property def name(self) -> Str: """Name of the user.""" return self._name @name.setter def name(self, value: Str): self._name = value @property def age(self) -> Int: """Age of the user.""" return self._age @age.setter def age(self, value: Int): self._age = value dasbus-1.7/examples/04_register/listener.py000066400000000000000000000013311433220137200210010ustar00rootroot00000000000000# # Handle changed properties. # Start the server, start the listener and run the client. # from dasbus.loop import EventLoop from common import REGISTER def callback(interface, changed_properties, invalid_properties): """The callback of the DBus signal PropertiesChanged.""" print("Properties of {} has changed: {}".format( interface, changed_properties )) if __name__ == "__main__": # Create a proxy of the object /org/example/Register # provided by the service org.example.Register proxy = REGISTER.get_proxy() # Connect the callback to the DBus signal PropertiesChanged. proxy.PropertiesChanged.connect(callback) # Start the event loop. loop = EventLoop() loop.run() dasbus-1.7/examples/04_register/server.py000066400000000000000000000045271433220137200204740ustar00rootroot00000000000000# # Run the service org.example.Register. # from dasbus.loop import EventLoop from dasbus.server.interface import dbus_interface from dasbus.server.property import emits_properties_changed from dasbus.server.template import InterfaceTemplate from dasbus.signal import Signal from dasbus.typing import Structure, List from dasbus.xml import XMLGenerator from common import SESSION_BUS, REGISTER, User, InvalidUser @dbus_interface(REGISTER.interface_name) class RegisterInterface(InterfaceTemplate): """The DBus interface of the user register.""" def connect_signals(self): """Connect the signals.""" self.watch_property("Users", self.implementation.users_changed) @property def Users(self) -> List[Structure]: """The list of users.""" return User.to_structure_list(self.implementation.users) @emits_properties_changed def RegisterUser(self, user: Structure): """Register a new user.""" self.implementation.register_user(User.from_structure(user)) class Register(object): """The implementation of the user register.""" def __init__(self): self._users = [] self._users_changed = Signal() @property def users(self): """The list of users.""" return self._users @property def users_changed(self): """Signal the user list change.""" return self._users_changed def register_user(self, user: User): """Register a new user.""" if any(u for u in self.users if u.name == user.name): raise InvalidUser("User {} exists.".format(user.name)) self._users.append(user) self._users_changed.emit() if __name__ == "__main__": # Print the generated XML specification. print(XMLGenerator.prettify_xml(RegisterInterface.__dbus_xml__)) try: # Create the register. register = Register() # Publish the register at /org/example/Register. SESSION_BUS.publish_object( REGISTER.object_path, RegisterInterface(register) ) # Register the service name org.example.Register. SESSION_BUS.register_service( REGISTER.service_name ) # Start the event loop. loop = EventLoop() loop.run() finally: # Unregister the DBus service and objects. SESSION_BUS.disconnect() dasbus-1.7/examples/05_chat/000077500000000000000000000000001433220137200157005ustar00rootroot00000000000000dasbus-1.7/examples/05_chat/client.py000066400000000000000000000017151433220137200175340ustar00rootroot00000000000000# # Send a message to the chat room. # from common import CHAT if __name__ == "__main__": # Create a proxy of the object /org/example/Chat # provided by the service org.example.Chat chat_proxy = CHAT.get_proxy() # Get an object path of the chat room. object_path = chat_proxy.FindRoom("Bob's room") print("Bob's room:", object_path) # Create a proxy of the object /org/example/Chat/Rooms/1 # provided by the service org.example.Chat room_proxy = CHAT.get_proxy(object_path) # Send a message to the chat room. room_proxy.SendMessage("Hi, I am Alice!") # Get an object path of the chat room. object_path = chat_proxy.FindRoom("Alice's room") print("Alice's room:", object_path) # Create a proxy of the object /org/example/Chat/Rooms/2 # provided by the service org.example.Chat room_proxy = CHAT.get_proxy(object_path) # Send a message to the chat room. room_proxy.SendMessage("I am Alice!") dasbus-1.7/examples/05_chat/common.py000066400000000000000000000012721433220137200175440ustar00rootroot00000000000000# # The common definitions # from dasbus.connection import SessionMessageBus from dasbus.identifier import DBusServiceIdentifier, DBusInterfaceIdentifier from dasbus.server.container import DBusContainer # Define the message bus. SESSION_BUS = SessionMessageBus() # Define namespaces. CHAT_NAMESPACE = ("org", "example", "Chat") ROOMS_NAMESPACE = (*CHAT_NAMESPACE, "Rooms") # Define services and objects. CHAT = DBusServiceIdentifier( namespace=CHAT_NAMESPACE, message_bus=SESSION_BUS ) ROOM = DBusInterfaceIdentifier( namespace=CHAT_NAMESPACE, basename="Room" ) # Define containers. ROOM_CONTAINER = DBusContainer( namespace=ROOMS_NAMESPACE, message_bus=SESSION_BUS ) dasbus-1.7/examples/05_chat/listener.py000066400000000000000000000016601433220137200201020ustar00rootroot00000000000000# # Reply to a message in the chat room. # Start the server, start the listener and run the client. # from dasbus.loop import EventLoop from common import CHAT def callback(proxy, msg): """The callback of the DBus signal MessageReceived.""" if "I am Alice!" in msg: proxy.SendMessage("Hello Alice, I am Bob.") if __name__ == "__main__": # Create a proxy of the object /org/example/Chat # provided by the service org.example.Chat. chat_proxy = CHAT.get_proxy() # Find a chat room to monitor. object_path = chat_proxy.FindRoom("Bob's room") # Create a proxy of the object /org/example/Chat/Rooms/1 # provided by the service org.example.Chat. room_proxy = CHAT.get_proxy(object_path) # Connect the callback to the DBus signal MessageReceived. room_proxy.MessageReceived.connect(lambda msg: callback(room_proxy, msg)) # Start the event loop. loop = EventLoop() loop.run() dasbus-1.7/examples/05_chat/server.py000066400000000000000000000056001433220137200175610ustar00rootroot00000000000000# # Run the service org.example.Chat. # from dasbus.loop import EventLoop from dasbus.server.interface import dbus_interface, dbus_signal from dasbus.server.publishable import Publishable from dasbus.server.template import InterfaceTemplate from dasbus.signal import Signal from dasbus.typing import Str, ObjPath from dasbus.xml import XMLGenerator from common import SESSION_BUS, CHAT, ROOM, ROOM_CONTAINER @dbus_interface(ROOM.interface_name) class RoomInterface(InterfaceTemplate): """The DBus interface of the chat room.""" def connect_signals(self): """Connect the signals.""" self.implementation.message_received.connect(self.MessageReceived) @dbus_signal def MessageReceived(self, msg: Str): """Signal that a message has been received.""" pass def SendMessage(self, msg: Str): """Send a message to the chat room.""" self.implementation.send_message(msg) class Room(Publishable): """The implementation of the chat room.""" def __init__(self, name): self._name = name self._message_received = Signal() def for_publication(self): """Return a DBus representation.""" return RoomInterface(self) @property def message_received(self): """Signal that a message has been received.""" return self._message_received def send_message(self, msg): """Send a message to the chat room.""" print("{}: {}".format(self._name, msg)) self.message_received.emit(msg) @dbus_interface(CHAT.interface_name) class ChatInterface(InterfaceTemplate): """The DBus interface of the chat service.""" def FindRoom(self, name: Str) -> ObjPath: """Find or create a chat room.""" return ROOM_CONTAINER.to_object_path( self.implementation.find_room(name) ) class Chat(Publishable): """The implementation of the chat.""" def __init__(self): self._rooms = {} def for_publication(self): """Return a DBus representation.""" return ChatInterface(self) def find_room(self, name): """Find or create a chat room.""" if name not in self._rooms: self._rooms[name] = Room(name) return self._rooms[name] if __name__ == "__main__": # Print the generated XML specifications. print(XMLGenerator.prettify_xml(ChatInterface.__dbus_xml__)) print(XMLGenerator.prettify_xml(RoomInterface.__dbus_xml__)) try: # Create the chat. chat = Chat() # Publish the chat at /org/example/Chat. SESSION_BUS.publish_object(CHAT.object_path, chat.for_publication()) # Register the service name org.example.Chat. SESSION_BUS.register_service(CHAT.service_name) # Start the event loop. loop = EventLoop() loop.run() finally: # Unregister the DBus service and objects. SESSION_BUS.disconnect() dasbus-1.7/examples/06_inhibit/000077500000000000000000000000001433220137200164105ustar00rootroot00000000000000dasbus-1.7/examples/06_inhibit/client.py000066400000000000000000000017671433220137200202530ustar00rootroot00000000000000# # Inhibit the system suspend and hibernation. # import os from dasbus.connection import SystemMessageBus from dasbus.unix import GLibClientUnix if __name__ == "__main__": # Create a representation of a system bus connection. bus = SystemMessageBus() # Create a proxy of the /org/freedesktop/login1 object # provided by the org.freedesktop.login1 service with # an enabled support for Unix file descriptors. proxy = bus.get_proxy( "org.freedesktop.login1", "/org/freedesktop/login1", client=GLibClientUnix ) # Inhibit sleep by this example. print("Inhibit sleep by my-example.") fd = proxy.Inhibit( "sleep", "my-example", "Running an example", "block" ) # List active inhibitors. print("Active inhibitors:") for inhibitor in sorted(proxy.ListInhibitors()): print("\t".join(map(str, inhibitor))) # Release the inhibition lock. print("Release the inhibition lock.") os.close(fd) dasbus-1.7/python-dasbus.spec000066400000000000000000000230571433220137200163220ustar00rootroot00000000000000%global srcname dasbus Name: python-%{srcname} Version: 1.7 Release: 1%{?dist} Summary: DBus library in Python 3 License: LGPL-2.1-or-later URL: https://pypi.python.org/pypi/dasbus %if %{defined suse_version} Source0: %{srcname}-%{version}.tar.gz Group: Development/Libraries/Python %else Source0: %{pypi_source} %endif BuildArch: noarch %global _description %{expand: Dasbus is a DBus library written in Python 3, based on GLib and inspired by pydbus. It is designed to be easy to use and extend.} %description %{_description} %package -n python3-%{srcname} Summary: %{summary} BuildRequires: python3-devel BuildRequires: python3-setuptools %if %{defined suse_version} BuildRequires: fdupes BuildRequires: python-rpm-macros Requires: python3-gobject %else Requires: python3-gobject-base %endif %{?python_provide:%python_provide python3-%{srcname}} %description -n python3-%{srcname} %{_description} %prep %autosetup -n %{srcname}-%{version} %build %py3_build %install %py3_install %if %{defined suse_version} %python_expand %fdupes %{buildroot}%{python3_sitelib} %endif %files -n python3-%{srcname} %license LICENSE %doc README.md %{python3_sitelib}/%{srcname}-*.egg-info/ %{python3_sitelib}/%{srcname}/ %changelog * Mon Nov 07 2022 Vendula Poncova - 1.7-1 - CI: Use dnf instead of yum to install CentOS packages (vponcova) - Documentation: Improve the installation instruction (vponcova) - Remove untracked files from the git repository interactively (vponcova) - UnixFD: Document the support for Unix file descriptors (vponcova) - Documentation: Clean up examples in the documentation (vponcova) - Documentation: Simplify the README.md file (vponcova) - Documentation: Fix bullet point lists (vponcova) - Documentation: Simplify the hostname example (vponcova) - CI: Run tests for all supported Python version (vponcova) - UnixFD: Handle DBus signals with Unix file descriptors (vponcova) - UnixFD: Add tests for DBus properties with Unix file descriptors (vponcova) - UnixFD: Clean up tests of DBus calls with Unix file descriptors (vponcova) - UnixFD: Clean up tests for swapping Unix file descriptors (vponcova) - UnixFD: Clean up `GLibClientUnix` and `GLibServerUnix` (vponcova) - UnixFD: Process results of client calls in the low-level library (vponcova) - UnixFD: Move the support for Unix file descriptors to dasbus.unix (vponcova) - CI: Always pull the latest container image (vponcova) - CI: Disable the unhashable-member warning (vponcova) - Revert "Don't use pylint from pip on Fedora Rawhide" (vponcova) - UnixFD: Move the unit tests to a new file (vponcova) - UnixFD: Manage the testing bus on set up and tear down (vponcova) - UnixFD: Don't add arguments to the DBusTestCase.setUp method (vponcova) - UnixFD: Create a new testing DBus interface (vponcova) - UnixFD: Fix the indentation in unit tests (vponcova) - Add unit tests for variants with variant types (vponcova) - Simplify the code for replacing values of the UnixFD type (vponcova) - Add classes for unpacking and unwrapping a variant (vponcova) - Don't use pylint from pip on Fedora Rawhide (vponcova) - UnixFD: Rename a parameter to server_arguments (vponcova) - UnixFD: Revert a change in GLibClient._async_call_finish (vponcova) - Raise TimeoutError if a DBus call times out (vponcova) - Fix pylint tests in CentOS Stream 8 (vponcova) - Fix the ENV instruction in Dockerfiles (vponcova) - Fix pylint issues (vponcova) - run forked tests using subprocess, instead of multiprocessing (wdouglass) - use mutable list for return value in fd_test_async make fd getters more explicit (wdouglass) - Add test case for method call only returning fd (jlyda) - Always use call_with_unix_fd_list* to properly handle returned fds (jlyda) - fix some lint discovered errors (wdouglass) - seperate unixfd functionality, to better support systems that don't have them (wdouglass) - Remove note in documentation about unsupported Unix file descriptors (wdouglass) - Add a test for UnixFD transfer (wdouglass) - Allow UnixFDs to be replaced and passed into Gio (wdouglass) - Fix rpm lint warnings for OpenSUSE 15.3 (christopher.m.cantalupo) - Extend the .coveragerc file (vponcova) - Disable builds for Fedora ELN on commits (vponcova) - Test Debian with Travis (vponcova) - Test Ubuntu with Travis (vponcova) - Test CentOS Stream 9 with Travis (vponcova) - Use CentOS Stream 8 for testing (vponcova) - add remove dbus object function on bus and update tests (matthewcaswell) - properly measure coverage across multiprocess test cases (wdouglass) - Move handle typing tests into a new class (and a new file) (wdouglass) - Add another test for a crazy data type, fix a bug discovered via the test (wdouglass) - Add functions for generating/consuming fdlists with variants (wdouglass) - Provide a language argument for the code blocks (seahawk1986) - Change the type of 'h' glib objects from 'File' to 'UnixFD' (wdouglass) - Allow to run tests in a container (vponcova) - Add C0209 to the ignore list for pylint (tjoslin) - Use the latest distro in Travis CI (vponcova) - Always update the container (vponcova) - Document limitations of the DBus specification generator (vponcova) * Mon May 31 2021 Vendula Poncova - 1.6-1 - Add support for SUSE packaging in spec file (christopher.m.cantalupo) - Allow to generate multiple output arguments (vponcova) - Support multiple output arguments (vponcova) - Add the is_tuple_of_one function (vponcova) - Configure the codecov tool (vponcova) * Mon May 03 2021 Vendula Poncova - 1.5-1 - Disable builds for Fedora ELN on pull requests (vponcova) - Provide additional info about the DBus call (vponcova) - Run the codecov uploader from a package (vponcova) - Switch to packit new fedora-latest alias (jkonecny) - Add daily builds for our Fedora-devel COPR repository (jkonecny) - Use Fedora container registry instead of Dockerhub (jkonecny) - Migrate daily COPR builds to Packit (jkonecny) - Switch Packit tests to copr builds instead (jkonecny) - Enable Packit build in ELN chroot (jkonecny) - Rename TestMessageBus class to silence pytest warning (luca) - Fix the raise-missing-from warning (vponcova) * Fri Jul 24 2020 Vendula Poncova - 1.4-1 - Handle all errors of the DBus call (vponcova) - Fix tests for handling DBus errors on the server side (vponcova) - Run packit smoke tests for all Fedora (jkonecny) - Fix packit archive creation (jkonecny) - Add possibility to change setup.py arguments (jkonecny) * Wed Jun 17 2020 Vendula Poncova - 1.3-1 - Document differences between dasbus and pydbus (vponcova) - Improve the support for interface proxies in the service identifier (vponcova) - Improve the support for interface proxies in the message bus (vponcova) - Test the interface proxies (vponcova) - Make the message bus of a service identifier accessible (vponcova) - Fix the testing environment for Fedora Rawhide (vponcova) * Mon May 18 2020 Vendula Poncova - 1.2-1 - Replace ABC with ABCMeta (vponcova) - Fix typing tests (vponcova) - Run tests on the latest CentOS (vponcova) - Install sphinx from PyPI (vponcova) * Thu May 14 2020 Vendula Poncova - 1.1-1 - Include tests and examples in the source distribution (vponcova) - Fix the pylint warning signature-differs (vponcova) * Tue May 05 2020 Vendula Poncova - 1.0-1 - Fix the documentation (vponcova) - Fix minor typos (yurchor) - Enable Codecov (vponcova) - Test the documentation build (vponcova) - Extend the documentation (vponcova) - Add configuration files for Read the Docs and Conda (vponcova) - Fix all warnings from the generated documentation (vponcova) * Wed Apr 08 2020 Vendula Poncova - 0.4-1 - Replace the error register with the error mapper (vponcova) - Propagate additional arguments for the client handler factory (vponcova) - Propagate additional arguments in the class AddressedMessageBus (vponcova) - Generate the documentation (vponcova) * Thu Apr 02 2020 Vendula Poncova - 0.3-1 - Remove generate_dictionary_from_data (vponcova) - Improve some of the error messages (vponcova) - Check the list of DBus structures to convert (vponcova) - Add the Inspiration section to README (vponcova) - Enable syntax highlighting in README (vponcova) - Use the class EventLoop in README (vponcova) - Use the --no-merges option (vponcova) - Clean up the Makefile (vponcova) - Add examples (vponcova) - Add the representation of the event loop (vponcova) - Enable copr builds and add packit config (dhodovsk) - Extend README (vponcova) * Mon Jan 13 2020 Vendula Poncova - 0.2-1 - Unwrap DBus values (vponcova) - Unwrap a variant data type (vponcova) - Add a default DBus error (vponcova) - Use the minimal image in Travis CI (vponcova) - Remove GLibErrorHandler (vponcova) - Remove map_error and map_by_default (vponcova) - Extend arguments of dbus_error (vponcova) - Extend arguments of dbus_interface (vponcova) - The list of callbacks in signals can be changed during emitting (vponcova) - Don't import from mock (vponcova) - Enable checks in Travis CI (vponcova) - Fix too long lines (vponcova) - Don't use wildcard imports (vponcova) - Add the check target to the Makefile (vponcova) - Enable Travis CI (vponcova) - Catch logged warnings in the unit tests (vponcova) - Add the coverage target to the Makefile (vponcova) - Rename tests (vponcova) - Create Makefile (vponcova) - Create a .spec file (vponcova) - Add requirements to the README file (vponcova) * Thu Oct 31 2019 Vendula Poncova - 0.1-1 - Initial package dasbus-1.7/setup.py000066400000000000000000000031011433220137200143440ustar00rootroot00000000000000# Copyright (C) 2019 Red Hat, Inc. All rights reserved. # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 # USA # from setuptools import setup, find_packages with open("README.md", "r") as f: long_description = f.read() setup( name="dasbus", version="1.7", author="Vendula Poncova", author_email="vponcova@redhat.com", description="DBus library in Python 3", long_description=long_description, long_description_content_type="text/markdown", keywords='dbus glib library', url="https://github.com/rhinstaller/dasbus", packages=find_packages(include=['dasbus', 'dasbus.*']), classifiers=[ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: GNU Lesser General" " Public License v2 or later (LGPLv2+)", "Programming Language :: Python :: 3", ], python_requires='>=3.6', ) dasbus-1.7/tests/000077500000000000000000000000001433220137200140015ustar00rootroot00000000000000dasbus-1.7/tests/__init__.py000066400000000000000000000000001433220137200161000ustar00rootroot00000000000000dasbus-1.7/tests/lib_dbus.py000066400000000000000000000122271433220137200161420ustar00rootroot00000000000000# # Copyright (C) 2022 Red Hat, Inc. All rights reserved. # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 # USA # import multiprocessing import sys import unittest from abc import abstractmethod, ABCMeta from contextlib import contextmanager from threading import Thread import gi gi.require_version("Gio", "2.0") gi.require_version("GLib", "2.0") from gi.repository import GLib, Gio def run_loop(timeout=3): """Run an event loop for the specified timeout. If any of the events fail or there are some pending events after the timeout, raise AssertionError. :param int timeout: a number of seconds """ loop = GLib.MainLoop() def _kill_loop(): loop.quit() return False GLib.timeout_add_seconds(timeout, _kill_loop) with catch_errors() as errors: loop.run() assert not errors, "The loop has failed!" assert not loop.get_context().pending() @contextmanager def catch_errors(): """Catch exceptions raised in this context. :return: a list of exceptions """ errors = [] def _handle_error(*exc_info): errors.append(exc_info) sys.__excepthook__(*exc_info) try: sys.excepthook = _handle_error yield errors finally: sys.excepthook = sys.__excepthook__ class AbstractDBusTestCase(unittest.TestCase, metaclass=ABCMeta): """Test DBus support with a real DBus connection.""" def setUp(self): """Set up the test.""" self.maxDiff = None # Initialize the service and the clients. self.service = self._get_service() self.clients = [] # Start a testing bus. self.bus = Gio.TestDBus() self.bus.up() # Create a connection to the testing bus. self.bus_address = self.bus.get_bus_address() self.message_bus = self._get_message_bus( self.bus_address ) @abstractmethod def _get_service(self): """Get a service.""" return None @classmethod @abstractmethod def _get_message_bus(cls, bus_address): """Get a testing message bus.""" return None @classmethod def _get_service_proxy(cls, message_bus, **proxy_args): """Get a proxy of the example service.""" return message_bus.get_proxy( "my.testing.Example", "/my/testing/Example", **proxy_args ) def _add_client(self, callback, *args, **kwargs): """Add a client.""" self.clients.append(callback) def _publish_service(self): """Publish the service on DBus.""" self.message_bus.publish_object( "/my/testing/Example", self.service ) self.message_bus.register_service( "my.testing.Example" ) def _run_test(self): """Run a test.""" self._publish_service() for client in self.clients: client.start() run_loop() for client in self.clients: client.join() def tearDown(self): """Tear down the test.""" if self.message_bus: self.message_bus.disconnect() if self.bus: self.bus.down() class DBusThreadedTestCase(AbstractDBusTestCase, metaclass=ABCMeta): """Test DBus support with a real DBus connection and threads.""" def _add_client(self, callback, *args, **kwargs): """Add a client thread.""" thread = Thread( target=callback, args=args, kwargs=kwargs, daemon=True, ) super()._add_client(thread) class DBusSpawnedTestCase(AbstractDBusTestCase, metaclass=ABCMeta): """Test DBus support with a real DBus connections and spawned processes.""" def setUp(self): """Set up the test.""" super().setUp() self.context = multiprocessing.get_context('spawn') def _add_client(self, callback, *args, **kwargs): """Add a client process.""" process = self.context.Process( name=callback.__name__, target=callback, args=(self.bus_address, *args), kwargs=kwargs, daemon=True, ) super()._add_client(process) def _run_test(self): """Run a test.""" super()._run_test() # Check the exit codes of the clients. for client in self.clients: msg = "{} has finished with {}".format( client.name, client.exitcode ) self.assertEqual(client.exitcode, 0, msg) dasbus-1.7/tests/test_client.py000066400000000000000000000526541433220137200167040ustar00rootroot00000000000000# # Copyright (C) 2019 Red Hat, Inc. All rights reserved. # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 # USA # import unittest from textwrap import dedent from unittest.mock import Mock from dasbus.client.handler import ClientObjectHandler, GLibClient from dasbus.client.proxy import ObjectProxy, disconnect_proxy, InterfaceProxy from dasbus.constants import DBUS_FLAG_NONE from dasbus.error import ErrorMapper, DBusError, ErrorRule from dasbus.signal import Signal from dasbus.specification import DBusSpecification from dasbus.typing import get_variant, get_variant_type, VariantType import gi gi.require_version("Gio", "2.0") gi.require_version("GLib", "2.0") from gi.repository import Gio, GLib class FakeException(Exception): """Fake exception from DBus calls.""" pass class VariantTypeFactory(object): """Return same objects for same type strings. This factory allows to easily test calls with variant types. """ def __init__(self): self._types = {} self._new = VariantType.new self._str = VariantType.__str__ self._repr = VariantType.__repr__ def set_up(self): VariantType.new = self.get VariantType.__str__ = VariantType.dup_string VariantType.__repr__ = VariantType.dup_string def tear_down(self): VariantType.new = self._new VariantType.__str__ = self._str VariantType.__repr__ = self._repr def get(self, type_string): return self._types.setdefault(type_string, self._new(type_string)) class DBusClientTestCase(unittest.TestCase): """Test DBus clinet support.""" NO_REPLY = get_variant("()", ()) def setUp(self): self.maxDiff = None self.message_bus = Mock() self.connection = self.message_bus.connection self.error_mapper = ErrorMapper() self.service_name = "my.service" self.object_path = "/my/object" self.handler = None self.proxy = None self.variant_type_factory = VariantTypeFactory() self.variant_type_factory.set_up() def tearDown(self): self.variant_type_factory.tear_down() def test_variant_type_factory(self): """Test the variant type factory.""" self.assertEqual(str(get_variant_type("s")), "s") self.assertEqual(repr(get_variant_type("i")), "i") self.assertEqual(get_variant_type("s"), get_variant_type("s")) self.assertEqual(get_variant_type("i"), get_variant_type("i")) self.assertNotEqual(get_variant_type("b"), get_variant_type("i")) self.assertNotEqual(get_variant_type("s"), get_variant_type("u")) def _create_proxy(self, xml): """Create a proxy with a mocked message bus.""" self.proxy = ObjectProxy( self.message_bus, self.service_name, self.object_path, error_mapper=self.error_mapper ) self.handler = self.proxy._handler self.handler._specification = DBusSpecification.from_xml(xml) def test_introspect(self): """Test the introspection.""" self._set_reply(get_variant("(s)", (dedent(""" """), ))) self.handler = ClientObjectHandler( self.message_bus, self.service_name, self.object_path ) self.assertIsNotNone(self.handler.specification) self._check_call( "org.freedesktop.DBus.Introspectable", "Introspect", reply_type=get_variant_type("(s)") ) self.assertIn( DBusSpecification.Method("Method1", "Interface", None, None), self.handler.specification.members ) def test_method(self): """Test the method proxy.""" self._create_proxy(""" """) self.assertTrue(callable(self.proxy.Method1)) self.assertEqual(self.proxy.Method1, self.proxy.Method1) self._set_reply(self.NO_REPLY) self.assertEqual(self.proxy.Method1(), None) self._check_call( "Interface", "Method1" ) self._set_reply(self.NO_REPLY) self.assertEqual(self.proxy.Method2(1), None) self._check_call( "Interface", "Method2", parameters=get_variant("(i)", (1, )) ) self._set_reply(get_variant("(i)", (0, ))) self.assertEqual(self.proxy.Method3(), 0) self._check_call( "Interface", "Method3", reply_type=get_variant_type("(i)") ) self._set_reply(get_variant("((ib))", ((1, True), ))) self.assertEqual(self.proxy.Method4([1.2, 2.3], "/my/path"), (1, True)) self._check_call( "Interface", "Method4", parameters=get_variant("(ado)", ([1.2, 2.3], "/my/path")), reply_type=get_variant_type("((ib))") ) self._set_reply(get_variant("(ii)", (1, 2))) self.assertEqual(self.proxy.Method5(), (1, 2)) self._check_call( "Interface", "Method5", reply_type=get_variant_type("(ii)") ) # Handle unregistered remote exception. self._set_reply(Gio.DBusError.new_for_dbus_error( "org.test.Unknown", "My message." )) with self.assertRaises(DBusError) as cm: self.proxy.Method1() self.assertTrue("My message." in str(cm.exception)) # Handle registered remote exception. self.error_mapper.add_rule(ErrorRule( exception_type=FakeException, error_name="org.test.Unknown" )) self._set_reply(Gio.DBusError.new_for_dbus_error( "org.test.Unknown", "My message." )) with self.assertRaises(FakeException) as cm: self.proxy.Method1() self.assertEqual(str(cm.exception), "My message.") # Handle timeout exception. self._set_reply(GLib.Error.new_literal( Gio.io_error_quark(), "Timeout was reached", Gio.IOErrorEnum.TIMED_OUT )) with self.assertRaises(TimeoutError) as cm: self.proxy.Method1() self.assertEqual( str(cm.exception), "The DBus call timeout was reached." ) # Handle local exception. self._set_reply(Exception("My message.")) with self.assertRaises(Exception) as cm: self.proxy.Method1() self.assertEqual(str(cm.exception), "My message.") # Test invalid method. with self.assertRaises(AttributeError) as cm: self.proxy.MethodInvalid() self.assertEqual( "DBus object has no attribute 'MethodInvalid'.", str(cm.exception) ) # Test invalid attribute. with self.assertRaises(AttributeError) as cm: self.proxy.Method1 = lambda: 1 self.assertEqual( "Can't set DBus attribute 'Method1'.", str(cm.exception) ) def test_invalid_method_result(self): """Test a method proxy with an invalid result.""" self._create_proxy(""" """) self._set_reply(get_variant("i", -1)) with self.assertRaises(TypeError): self.proxy.Method() def _set_reply(self, reply_value): """Set the reply of the DBus call.""" self.connection.call_sync.reset_mock() if isinstance(reply_value, Exception): self.connection.call_sync.side_effect = reply_value else: self.connection.call_sync.return_value = reply_value def _check_call(self, interface_name, method_name, parameters=None, reply_type=None): """Check the DBus call.""" self.connection.call_sync.assert_called_once_with( self.service_name, self.object_path, interface_name, method_name, parameters, reply_type, DBUS_FLAG_NONE, GLibClient.DBUS_TIMEOUT_NONE, None ) self.connection.call_sync.reset_mock() def test_async_method(self): """Test asynchronous calls of a method proxy.""" self._create_proxy(""" """) callback = Mock() callback_args = ("A", "B") self.proxy.Method1(callback=callback, callback_args=callback_args) self._check_async_call( "Interface", "Method1", callback, callback_args ) self._finish_async_call(self.NO_REPLY, callback, callback_args) callback.assert_called_once_with(None, "A", "B") callback = Mock() callback_args = ("A", "B") self.proxy.Method2( 1, 2, callback=callback, callback_args=callback_args ) self._check_async_call( "Interface", "Method2", callback, callback_args, get_variant("(ii)", (1, 2)), get_variant_type("(i)") ) self._finish_async_call( get_variant("(i)", (3, )), callback, callback_args ) callback.assert_called_once_with(3, "A", "B") self.error_mapper.add_rule(ErrorRule( exception_type=FakeException, error_name="org.test.Unknown" )) callback = Mock() callback_args = ("A", "B") error = Gio.DBusError.new_for_dbus_error( "org.test.Unknown", "My message." ) with self.assertRaises(FakeException) as cm: self._finish_async_call(error, callback, callback_args) self.assertEqual(str(cm.exception), "My message.") callback.assert_not_called() def _check_async_call(self, interface_name, method_name, callback, callback_args, parameters=None, reply_type=None): """Check the asynchronous DBus call.""" self.connection.call.assert_called_once_with( self.service_name, self.object_path, interface_name, method_name, parameters, reply_type, DBUS_FLAG_NONE, GLibClient.DBUS_TIMEOUT_NONE, callback=GLibClient._async_call_finish, user_data=( self.handler._method_callback, (callback, callback_args) ) ) self.connection.call.reset_mock() def _finish_async_call(self, result, callback, callback_args): """Finish the asynchronous call.""" def _call_finish(result_object): if isinstance(result_object, Exception): raise result_object return result_object def _callback(finish, *args): callback(finish(), *args) GLibClient._async_call_finish( source_object=Mock(call_finish=_call_finish), result_object=result, user_data=( self.handler._method_callback, (_callback, callback_args) ) ) def test_property(self): """Test the property proxy.""" self._create_proxy(""" """) self._set_reply(self.NO_REPLY) self.proxy.Property1 = 10 self._check_set_property("Property1", get_variant("i", 10)) self._set_reply(get_variant("(v)", (get_variant("i", 20), ))) self.assertEqual(self.proxy.Property1, 20) self._check_get_property("Property1") with self.assertRaises(AttributeError) as cm: self.proxy.Property2 = "World" self.assertEqual(str(cm.exception), "Can't set DBus property.") self._set_reply(get_variant("(v)", (get_variant("s", "Hello"), ))) self.assertEqual(self.proxy.Property2, "Hello") self._check_get_property("Property2") self._set_reply(self.NO_REPLY) self.proxy.Property3 = False self._check_set_property("Property3", get_variant("b", False)) with self.assertRaises(AttributeError) as cm: self.fail(self.proxy.Property3) self.assertEqual(str(cm.exception), "Can't read DBus property.") with self.assertRaises(AttributeError) as cm: self.proxy.PropertyInvalid = 0 self.assertEqual( "DBus object has no attribute 'PropertyInvalid'.", str(cm.exception) ) with self.assertRaises(AttributeError) as cm: self.fail(self.proxy.PropertyInvalid) self.assertEqual( "DBus object has no attribute 'PropertyInvalid'.", str(cm.exception) ) def _check_set_property(self, name, value): """Check the DBus call that sets a property.""" self._check_call( "org.freedesktop.DBus.Properties", "Set", get_variant("(ssv)", ("Interface", name, value)), None ) def _check_get_property(self, name): """Check the DBus call that gets a property.""" self._check_call( "org.freedesktop.DBus.Properties", "Get", get_variant("(ss)", ("Interface", name)), get_variant_type("(v)") ) def test_signal(self): """Test the signal publishing.""" self._create_proxy(""" """) self.assertIsInstance(self.proxy.Signal1, Signal) self.assertEqual(self.proxy.Signal1, self.proxy.Signal1) self._check_signal("Interface", "Signal1", self.proxy.Signal1.emit) self._emit_signal(self.NO_REPLY, self.proxy.Signal1.emit) self.assertEqual(len(self.handler._subscriptions), 2) self._check_signal("Interface", "Signal2", self.proxy.Signal2.emit) self._emit_signal(get_variant("(is)", (1, "Test")), self.proxy.Signal2.emit) self.assertEqual(len(self.handler._subscriptions), 4) with self.assertRaises(AttributeError) as cm: self.fail(self.proxy.SignalInvalid) self.assertEqual( "DBus object has no attribute 'SignalInvalid'.", str(cm.exception) ) with self.assertRaises(AttributeError) as cm: self.proxy.Signal1 = self.handler._signal_factory() self.assertEqual( "Can't set DBus attribute 'Signal1'.", str(cm.exception) ) self.proxy.Signal1.connect(Mock()) self.proxy.Signal2.connect(Mock()) disconnect_proxy(self.proxy) self.assertEqual(self.connection.signal_unsubscribe.call_count, 2) self.assertEqual(self.handler._subscriptions, []) self.assertEqual(self.proxy.Signal1._callbacks, []) self.assertEqual(self.proxy.Signal2._callbacks, []) def _check_signal(self, interface_name, signal_name, signal_callback): """Check the DBus signal subscription.""" self.connection.signal_subscribe.assert_called_once_with( self.service_name, interface_name, signal_name, self.object_path, None, DBUS_FLAG_NONE, callback=GLibClient._signal_callback, user_data=(self.handler._signal_callback, (signal_callback, )) ) self.connection.signal_subscribe.reset_mock() def _emit_signal(self, parameters, signal_callback): """Emit a DBus signal.""" GLibClient._signal_callback( self.connection, None, self.object_path, None, None, parameters=parameters, user_data=(self.handler._signal_callback, (signal_callback,)) ) def test_error(self): """Test the error handling.""" error = Exception("My message.") self.assertEqual( GLibClient.is_remote_error(error), False ) error = Gio.DBusError.new_for_dbus_error( "org.test.Error", "My message." ) self.assertEqual( GLibClient.is_remote_error(error), True ) self.assertEqual( GLibClient.get_remote_error_name(error), "org.test.Error", ) self.assertEqual( GLibClient.get_remote_error_message(error), "My message." ) def _create_interface_proxy(self, xml, interface_name): """Create an interface proxy with a mocked message bus.""" self.proxy = InterfaceProxy( self.message_bus, self.service_name, self.object_path, interface_name=interface_name, error_mapper=self.error_mapper ) self.handler = self.proxy._handler self.handler._specification = DBusSpecification.from_xml(xml) def test_interface_proxy(self): """Test the interface proxy.""" xml = """ """ # Test the first interface. self._create_interface_proxy(xml, "Interface1") # Test a valid method. self._set_reply(self.NO_REPLY) self.assertEqual(self.proxy.Method1(), None) self._check_call("Interface1", "Method1") # Test invalid methods. with self.assertRaises(AttributeError): self.proxy.Method2() with self.assertRaises(AttributeError): self.proxy.Method3() # Test the second interface. self._create_interface_proxy(xml, "Interface2") # Test a valid method. self._set_reply(self.NO_REPLY) self.assertEqual(self.proxy.Method2(), None) self._check_call("Interface2", "Method2") # Test invalid methods. with self.assertRaises(AttributeError): self.proxy.Method1() with self.assertRaises(AttributeError): self.proxy.Method3() # Test the third interface. self._create_interface_proxy(xml, "Interface3") # Test a valid method. self._set_reply(self.NO_REPLY) self.assertEqual(self.proxy.Method1(), None) self._check_call("Interface3", "Method1") self._set_reply(self.NO_REPLY) self.assertEqual(self.proxy.Method2(), None) self._check_call("Interface3", "Method2") self._set_reply(self.NO_REPLY) self.assertEqual(self.proxy.Method3(), None) self._check_call("Interface3", "Method3") # Test an invalid method. with self.assertRaises(AttributeError): self.proxy.Method4() dasbus-1.7/tests/test_connection.py000066400000000000000000000250061433220137200175540ustar00rootroot00000000000000# # Copyright (C) 2019 Red Hat, Inc. All rights reserved. # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 # USA # import unittest from collections import defaultdict from unittest.mock import Mock, patch from dasbus.connection import MessageBus, SystemMessageBus, \ SessionMessageBus, AddressedMessageBus from dasbus.constants import DBUS_REQUEST_NAME_REPLY_PRIMARY_OWNER, \ DBUS_NAME_FLAG_ALLOW_REPLACEMENT, DBUS_REQUEST_NAME_REPLY_ALREADY_OWNER from dasbus.error import ErrorMapper import gi gi.require_version("Gio", "2.0") from gi.repository import Gio class MockMessageBus(MessageBus): """Message bus for testing.""" def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._proxy_factory = Mock() self._server_factory = Mock() def _get_connection(self): return Mock() def publish_object(self, *args, **kwargs): # pylint: disable=signature-differs return super().publish_object( *args, **kwargs, server_factory=self._server_factory ) def get_proxy(self, *args, **kwargs): # pylint: disable=signature-differs return super().get_proxy( *args, **kwargs, proxy_factory=self._proxy_factory ) class DBusConnectionTestCase(unittest.TestCase): """Test DBus connection.""" def setUp(self): self.message_bus = MockMessageBus() self.error_mapper = self.message_bus._error_mapper self.proxy_factory = self.message_bus._proxy_factory self.server_factory = self.message_bus._server_factory def test_connection(self): """Test the bus connection.""" self.assertIsNotNone(self.message_bus.connection) self.assertEqual(self.message_bus.connection, self.message_bus.connection) self.assertTrue(self.message_bus.check_connection()) def test_failing_connection(self): """Test the failing connection.""" self.message_bus._get_connection = Mock(side_effect=IOError()) with self.assertLogs(level='WARN'): self.assertFalse(self.message_bus.check_connection()) self.message_bus._get_connection = Mock(return_value=None) self.assertFalse(self.message_bus.check_connection()) def test_error_mapper(self): """Test the error mapper.""" error_mapper = ErrorMapper() message_bus = MockMessageBus(error_mapper=error_mapper) self.assertEqual(message_bus._error_mapper, error_mapper) message_bus = MockMessageBus() self.assertNotEqual(message_bus._error_mapper, error_mapper) self.assertIsInstance(message_bus._error_mapper, ErrorMapper) def test_proxy(self): """Test the object proxy.""" proxy = self.message_bus.get_proxy( "service.name", "/object/path" ) self.proxy_factory.assert_called_once_with( self.message_bus, "service.name", "/object/path", error_mapper=self.error_mapper ) self.assertEqual(proxy, self.proxy_factory.return_value) def test_interface_proxy(self): """Test the interface proxy.""" proxy = self.message_bus.get_proxy( "service.name", "/object/path", "interface.name" ) self.proxy_factory.assert_called_once_with( self.message_bus, "service.name", "/object/path", interface_name="interface.name", error_mapper=self.error_mapper ) self.assertEqual(proxy, self.proxy_factory.return_value) def test_bus_proxy(self): """Test the bus proxy.""" proxy = self.message_bus.proxy self.proxy_factory.assert_called_once_with( self.message_bus, "org.freedesktop.DBus", "/org/freedesktop/DBus", error_mapper=self.error_mapper ) self.assertIsNotNone(proxy) self.assertEqual(proxy, self.proxy_factory.return_value) self.assertEqual(self.message_bus.proxy, self.message_bus.proxy) def test_register_service(self): """Test the service registration.""" self.message_bus.proxy.RequestName.return_value = \ DBUS_REQUEST_NAME_REPLY_PRIMARY_OWNER self.message_bus.register_service( "my.service", DBUS_NAME_FLAG_ALLOW_REPLACEMENT ) self.message_bus.proxy.RequestName.assert_called_once_with( "my.service", DBUS_NAME_FLAG_ALLOW_REPLACEMENT ) self.assertIn("my.service", self.message_bus._requested_names) callback = self.message_bus._registrations["my.service"] self.assertTrue(callable(callback)) self.message_bus.disconnect() self.message_bus.proxy.ReleaseName.assert_called_once_with( "my.service" ) def test_failed_register_service(self): """Test the failing service registration.""" self.message_bus.proxy.RequestName.return_value = \ DBUS_REQUEST_NAME_REPLY_ALREADY_OWNER with self.assertRaises(ConnectionError): self.message_bus.register_service("my.service") self.message_bus.proxy.RequestName.assert_called_once_with( "my.service", DBUS_NAME_FLAG_ALLOW_REPLACEMENT ) self.assertNotIn("my.service", self.message_bus._requested_names) def test_check_service_access(self): """Check the service access.""" # The service can be accessed. self.message_bus.get_proxy("my.service", "/my/object") # The service cannot be accessed. self.message_bus.proxy.RequestName.return_value = \ DBUS_REQUEST_NAME_REPLY_PRIMARY_OWNER self.message_bus.register_service("my.service") with self.assertRaises(RuntimeError) as cm: self.message_bus.get_proxy("my.service", "/my/object") self.assertEqual( "Can't access DBus service 'my.service' from the main thread.", str(cm.exception) ) def test_publish_object(self): """Test the object publishing.""" obj = Mock() self.message_bus.publish_object("/my/object", obj) self.server_factory.assert_called_once_with( self.message_bus, "/my/object", obj, error_mapper=self.error_mapper ) callback = self.message_bus._registrations["/my/object"] self.assertTrue(callable(callback)) self.message_bus.disconnect() callback.assert_called_once_with() def test_disconnect(self): """Test the disconnection.""" # Set up the connection. self.assertIsNotNone(self.message_bus.connection) # Create registrations. callbacks = defaultdict(Mock) self.message_bus._registrations = { "my.service.1": callbacks["my.service.1"], "my.service.2": callbacks["my.service.2"], "/my/object/1": callbacks["/my/object/1"], "/my/object/2": callbacks["/my/object/2"], } self.message_bus._requested_names = { "my.service.1", "my.service.2" } # Disconnect. self.message_bus.disconnect() self.assertEqual(self.message_bus._connection, None) self.assertEqual(self.message_bus._registrations, {}) self.assertEqual(self.message_bus._requested_names, set()) for callback in callbacks.values(): callback.assert_called_once_with() # Do nothing by default. self.message_bus.disconnect() def test_remove_single_object(self): """Test the disconnection.""" # Set up the connection. self.assertIsNotNone(self.message_bus.connection) # Create registrations. callbacks = defaultdict(Mock) self.message_bus._registrations = { "my.service.1": callbacks["my.service.1"], "my.service.2": callbacks["my.service.2"], "/my/object/1": callbacks["/my/object/1"], "/my/object/2": callbacks["/my/object/2"], } self.message_bus._requested_names = { "my.service.1", "my.service.2" } # Disconnect. self.message_bus.unregister_service("my.service.1") self.assertEqual(len(self.message_bus._registrations), 3) # Disconnect. self.message_bus.unpublish_object("/my/object/2") self.assertEqual(len(self.message_bus._registrations), 2) callbacks["my.service.1"].assert_called_once_with() callbacks["/my/object/2"].assert_called_once_with() @patch("dasbus.connection.Gio.bus_get_sync") def test_system_bus(self, getter): """Test the system bus.""" message_bus = SystemMessageBus() self.assertIsNotNone(message_bus.connection) getter.assert_called_once_with( Gio.BusType.SYSTEM, None ) @patch("dasbus.connection.Gio.bus_get_sync") def test_session_bus(self, getter): """Test the session bus.""" message_bus = SessionMessageBus() self.assertIsNotNone(message_bus.connection) getter.assert_called_once_with( Gio.BusType.SESSION, None ) def _check_addressed_connection(self, message_bus, getter, address): self.assertIsNotNone(message_bus.connection) self.assertEqual(message_bus.address, address) getter.assert_called_once_with( address, ( Gio.DBusConnectionFlags.AUTHENTICATION_CLIENT | Gio.DBusConnectionFlags.MESSAGE_BUS_CONNECTION ), None, None ) @patch("dasbus.connection.Gio.DBusConnection.new_for_address_sync") def test_addressed_bus(self, getter): """Test the addressed bus.""" message_bus = AddressedMessageBus("ADDRESS") self._check_addressed_connection(message_bus, getter, "ADDRESS") dasbus-1.7/tests/test_container.py000066400000000000000000000145531433220137200174040ustar00rootroot00000000000000# # Copyright (C) 2019 Red Hat, Inc. All rights reserved. # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 # USA # import unittest from unittest.mock import Mock from dasbus.server.container import DBusContainer, DBusContainerError from dasbus.server.interface import dbus_interface from dasbus.server.publishable import Publishable from dasbus.server.template import BasicInterfaceTemplate from dasbus.typing import Str, ObjPath @dbus_interface("org.Project.Object") class MyInterface(BasicInterfaceTemplate): def HelloWorld(self) -> Str: return self.implementation.hello_world() class MyObject(Publishable): def for_publication(self): return MyInterface(self) def hello_world(self): return "Hello World!" class MyUnpublishable(object): pass class DBusContainerTestCase(unittest.TestCase): """Test DBus containers.""" def setUp(self): self.message_bus = Mock() self.container = DBusContainer( namespace=("org", "Project"), basename="Object", message_bus=self.message_bus ) def test_set_namespace(self): """Test set_namespace.""" self.container.set_namespace(("org", "Another", "Project")) path = self.container.to_object_path(MyObject()) self.assertEqual(path, "/org/Another/Project/Object/1") path = self.container.to_object_path(MyObject()) self.assertEqual(path, "/org/Another/Project/Object/2") def test_to_object_path_failed(self): """Test failed to_object_path.""" with self.assertRaises(TypeError) as cm: self.container.to_object_path(MyUnpublishable()) self.assertEqual( "Type 'MyUnpublishable' is not publishable.", str(cm.exception) ) with self.assertRaises(DBusContainerError) as cm: self.container._find_object_path(MyObject()) self.assertEqual( "No object path found.", str(cm.exception) ) def test_to_object_path(self): """Test to_object_path.""" obj = MyObject() path = self.container.to_object_path(obj) self.message_bus.publish_object.assert_called_once() published_path, published_obj = \ self.message_bus.publish_object.call_args[0] self.assertEqual(path, "/org/Project/Object/1") self.assertEqual(path, published_path) self.assertIsInstance(published_obj, MyInterface) self.assertEqual(obj, published_obj.implementation) self.message_bus.reset_mock() self.assertEqual(self.container.to_object_path(obj), path) self.message_bus.publish_object.assert_not_called() self.assertEqual(self.container.to_object_path(obj), path) self.message_bus.publish_object.assert_not_called() def test_to_object_path_list(self): """Test to_object_path_list.""" objects = [MyObject(), MyObject(), MyObject()] paths = self.container.to_object_path_list(objects) self.assertEqual(self.message_bus.publish_object.call_count, 3) self.assertEqual(paths, [ "/org/Project/Object/1", "/org/Project/Object/2", "/org/Project/Object/3" ]) self.message_bus.reset_mock() self.assertEqual(paths, self.container.to_object_path_list(objects)) self.message_bus.publish_object.assert_not_called() self.assertEqual(paths, self.container.to_object_path_list(objects)) self.message_bus.publish_object.assert_not_called() def test_from_object_path_failed(self): """Test failures.""" with self.assertRaises(DBusContainerError) as cm: self.container.from_object_path(ObjPath("/org/Project/Object/1")) self.assertEqual( "Unknown object path '/org/Project/Object/1'.", str(cm.exception) ) def test_from_object_path(self): """Test from_object_path.""" obj = MyObject() path = self.container.to_object_path(obj) self.assertEqual(obj, self.container.from_object_path(path)) self.assertEqual(path, self.container.to_object_path(obj)) self.assertEqual(obj, self.container.from_object_path(path)) self.assertEqual(path, self.container.to_object_path(obj)) def test_from_object_path_list(self): """Test from_object_path_list.""" objects = [MyObject(), MyObject(), MyObject()] paths = self.container.to_object_path_list(objects) self.assertEqual(objects, self.container.from_object_path_list(paths)) self.assertEqual(paths, self.container.to_object_path_list(objects)) self.assertEqual(objects, self.container.from_object_path_list(paths)) self.assertEqual(paths, self.container.to_object_path_list(objects)) def test_multiple_objects(self): """Test multiple objects.""" obj = MyObject() path = self.container.to_object_path(obj) self.assertEqual(path, "/org/Project/Object/1") self.assertEqual(obj, self.container.from_object_path(path)) self.message_bus.publish_object.assert_called_once() self.message_bus.reset_mock() obj = MyObject() path = self.container.to_object_path(obj) self.assertEqual(path, "/org/Project/Object/2") self.assertEqual(obj, self.container.from_object_path(path)) self.message_bus.publish_object.assert_called_once() self.message_bus.reset_mock() obj = MyObject() path = self.container.to_object_path(obj) self.assertEqual(path, "/org/Project/Object/3") self.assertEqual(obj, self.container.from_object_path(path)) self.message_bus.publish_object.assert_called_once() self.message_bus.reset_mock() dasbus-1.7/tests/test_dbus.py000066400000000000000000000360161433220137200163550ustar00rootroot00000000000000# # Copyright (C) 2019 Red Hat, Inc. All rights reserved. # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 # USA # from unittest import mock from unittest.mock import Mock from dasbus.client.proxy import disconnect_proxy from dasbus.connection import AddressedMessageBus from dasbus.error import ErrorMapper, get_error_decorator from dasbus.server.interface import dbus_interface, dbus_signal, \ accepts_additional_arguments, returns_multiple_arguments from dasbus.typing import get_variant, Str, Int, Dict, Variant, List, \ Tuple, Bool from dasbus.xml import XMLGenerator from threading import Event from tests.lib_dbus import DBusThreadedTestCase # Define the error mapper and decorator. error_mapper = ErrorMapper() dbus_error = get_error_decorator(error_mapper) @dbus_error("my.testing.Error") class ExampleException(Exception): pass @dbus_interface("my.testing.Example") class ExampleInterface(object): def __init__(self): self._knocked = False self._names = [] self._values = [0] self._secrets = [] @property def Name(self) -> Str: return "My example" @property def Value(self) -> Int: return self._values[-1] @Value.setter def Value(self, value: Int): self._values.append(value) self.PropertiesChanged( "my.testing.Example", {"Value": get_variant(Int, value)}, ["Name"] ) def _set_secret(self, secret: Str): self._secrets.append(secret) Secret = property(fset=_set_secret) def Knock(self): self._knocked = True self.Knocked() def Hello(self, name: Str) -> Str: self._names.append(name) self.Visited(name) return "Hello, {0}!".format(name) @dbus_signal def Knocked(self): pass @dbus_signal def Visited(self, name: Str): pass def Raise(self, message: Str): raise ExampleException(message) @dbus_signal def PropertiesChanged(self, interface: Str, changed: Dict[Str, Variant], invalid: List[Str]): pass @accepts_additional_arguments def GetInfo(self, arg: Str, *, call_info) -> Str: return "{0}: {1}".format(arg, call_info) @returns_multiple_arguments def ReturnArgs(self) -> Tuple[Int, Bool, Str]: return 0, False, "zero" class DBusExampleTestCase(DBusThreadedTestCase): """Test DBus support with a real DBus connection.""" def _get_service(self): """Get the example service.""" return ExampleInterface() @classmethod def _get_message_bus(cls, bus_address): """Get a message bus.""" return AddressedMessageBus(bus_address, error_mapper) def _get_proxy(self, **proxy_args): """Get a proxy of the example service.""" return self._get_service_proxy(self.message_bus, **proxy_args) def test_message_bus(self): """Test the message bus.""" self.assertTrue(self.message_bus.check_connection()) self.assertEqual(self.message_bus.address, self.bus.get_bus_address()) self.assertEqual(self.message_bus.proxy.Ping(), None) def test_xml_specification(self): """Test the generated specification.""" expected_xml = ''' ''' generated_xml = self.service.__dbus_xml__ self.assertEqual( XMLGenerator.prettify_xml(expected_xml), XMLGenerator.prettify_xml(generated_xml) ) def test_knock(self): """Call a simple DBus method.""" self.assertEqual(self.service._knocked, False) def test(): proxy = self._get_proxy() self.assertEqual(None, proxy.Knock()) self._add_client(test) self._run_test() self.assertEqual(self.service._knocked, True) def test_hello(self): """Call a DBus method.""" def test1(): proxy = self._get_proxy() self.assertEqual("Hello, Foo!", proxy.Hello("Foo")) def test2(): proxy = self._get_proxy() self.assertEqual("Hello, Bar!", proxy.Hello("Bar")) self._add_client(test1) self._add_client(test2) self._run_test() self.assertEqual(sorted(self.service._names), ["Bar", "Foo"]) def test_timeout(self): """Call a DBus method with a timeout.""" def test1(): proxy = self._get_proxy() proxy.Hello("Foo", timeout=1000) def test2(): proxy = self._get_proxy() with self.assertRaises(TimeoutError): proxy.Hello("Bar", timeout=0) self._add_client(test1) self._add_client(test2) self._run_test() self.assertEqual(sorted(self.service._names), ["Bar", "Foo"]) def test_name(self): """Use a DBus read-only property.""" def test1(): proxy = self._get_proxy() self.assertEqual("My example", proxy.Name) def test2(): proxy = self._get_proxy() self.assertEqual("My example", proxy.Name) def test3(): proxy = self._get_proxy() with self.assertRaises(AttributeError) as cm: proxy.Name = "Another example" self.assertEqual( "Can't set DBus property.", str(cm.exception) ) self.assertEqual("My example", proxy.Name) self._add_client(test1) self._add_client(test2) self._add_client(test3) self._run_test() def test_secret(self): """Use a DBus write-only property.""" def test1(): proxy = self._get_proxy() proxy.Secret = "Secret 1" def test2(): proxy = self._get_proxy() proxy.Secret = "Secret 2" def test3(): proxy = self._get_proxy() with self.assertRaises(AttributeError) as cm: self.fail(proxy.Secret) self.assertEqual( "Can't read DBus property.", str(cm.exception) ) self._add_client(test1) self._add_client(test2) self._add_client(test3) self._run_test() self.assertEqual(sorted(self.service._secrets), [ "Secret 1", "Secret 2" ]) def test_value(self): """Use a DBus read-write property.""" def test1(): proxy = self._get_proxy() self.assertIn(proxy.Value, (0, 3, 4)) proxy.Value = 1 self.assertIn(proxy.Value, (1, 3, 4)) proxy.Value = 2 self.assertIn(proxy.Value, (2, 3, 4)) def test2(): proxy = self._get_proxy() self.assertIn(proxy.Value, (0, 1, 2)) proxy.Value = 3 self.assertIn(proxy.Value, (3, 1, 2)) proxy.Value = 4 self.assertIn(proxy.Value, (4, 1, 2)) self._add_client(test1) self._add_client(test2) self._run_test() self.assertEqual(sorted(self.service._values), [0, 1, 2, 3, 4]) self.assertEqual(self.service._values[0], 0) self.assertLess(self.service._values.index(1), self.service._values.index(2)) self.assertLess(self.service._values.index(3), self.service._values.index(4)) def test_knocked(self): """Use a simple DBus signal.""" event = Event() knocked = Mock() def callback(): knocked("Knocked!") def test_1(): proxy = self._get_proxy() proxy.Knocked.connect(callback) event.set() def test_2(): event.wait() proxy = self._get_proxy() proxy.Knock() proxy.Knock() proxy.Knock() self._add_client(test_1) self._add_client(test_2) self._run_test() knocked.assert_has_calls([ mock.call("Knocked!"), mock.call("Knocked!"), mock.call("Knocked!") ]) def test_visited(self): """Use a DBus signal.""" event = Event() visited = Mock() def callback(name): visited("Visited by {0}.".format(name)) def test1(): proxy = self._get_proxy() proxy.Visited.connect(callback) event.set() def test2(): event.wait() proxy = self._get_proxy() proxy.Hello("Foo") proxy.Hello("Bar") self._add_client(test1) self._add_client(test2) self._run_test() visited.assert_has_calls([ mock.call("Visited by Foo."), mock.call("Visited by Bar.") ]) def test_unsubscribed(self): """Use an unsubscribed DBus signal.""" event = Event() knocked = Mock() def callback(): knocked("Knocked!") def test_1(): proxy = self._get_proxy() proxy.Knocked.connect(callback) disconnect_proxy(proxy) event.set() def test_2(): event.wait() proxy = self._get_proxy() proxy.Knock() proxy.Knock() proxy.Knock() self._add_client(test_1) self._add_client(test_2) self._run_test() knocked.assert_not_called() def test_asynchronous(self): """Call a DBus method asynchronously.""" returned = Mock() def callback(call, number): returned(number, call()) def test(): proxy = self._get_proxy() proxy.Hello("Foo", callback=callback, callback_args=(1, )) proxy.Hello("Foo", callback=callback, callback_args=(2, )) proxy.Hello("Bar", callback=callback, callback_args=(3, )) self._add_client(test) self._run_test() returned.assert_has_calls([ mock.call(1, "Hello, Foo!"), mock.call(2, "Hello, Foo!"), mock.call(3, "Hello, Bar!"), ]) def test_error(self): """Handle a DBus error.""" raised = Mock() def callback(call, number): try: call() except ExampleException as e: raised(number, str(e)) def test1(): proxy = self._get_proxy() proxy.Raise("Foo failed!", callback=callback, callback_args=(1, )) proxy.Raise("Foo failed!", callback=callback, callback_args=(2, )) proxy.Raise("Bar failed!", callback=callback, callback_args=(3, )) def test2(): proxy = self._get_proxy() try: proxy.Raise("My message") except ExampleException as e: self.assertEqual(str(e), "My message") else: self.fail("Exception wasn't raised!") self._add_client(test1) self._add_client(test2) with self.assertLogs(level='WARN'): self._run_test() raised.assert_has_calls([ mock.call(1, "Foo failed!"), mock.call(2, "Foo failed!"), mock.call(3, "Bar failed!"), ]) def test_properties_changed(self): """Test the PropertiesChanged signal.""" event = Event() callback = Mock() def test_1(): proxy = self._get_proxy() proxy.PropertiesChanged.connect(callback) event.set() def test_2(): event.wait() proxy = self._get_proxy() proxy.Value = 10 self._add_client(test_1) self._add_client(test_2) self._run_test() callback.assert_called_once_with( "my.testing.Example", {"Value": get_variant(Int, 10)}, ["Name"] ) def test_interface(self): """Use a specific DBus interface.""" def test_1(): proxy = self._get_proxy(interface_name="my.testing.Example") proxy.Knock() with self.assertRaises(AttributeError): proxy.Ping() def test_2(): proxy = self._get_proxy(interface_name="org.freedesktop.DBus.Peer") proxy.Ping() with self.assertRaises(AttributeError): proxy.Knock() self._add_client(test_1) self._add_client(test_2) self._run_test() def test_additional_arguments(self): """Call a DBus method.""" def test1(): proxy = self._get_proxy() self.assertEqual( proxy.GetInfo("Foo"), "Foo: {'sender': ':1.0'}" ) def test2(): proxy = self._get_proxy() self.assertEqual( proxy.GetInfo("Bar"), "Bar: {'sender': ':1.0'}" ) self._add_client(test1) self._add_client(test2) self._run_test() def test_multiple_output_arguments(self): """Call a DBus method with multiple output arguments.""" def test1(): proxy = self._get_proxy() self.assertEqual( proxy.ReturnArgs(), (0, False, "zero") ) self._add_client(test1) self._run_test() dasbus-1.7/tests/test_error.py000066400000000000000000000144121433220137200165450ustar00rootroot00000000000000# # Copyright (C) 2019 Red Hat, Inc. All rights reserved. # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 # USA # import unittest from dasbus.error import ErrorMapper, DBusError, get_error_decorator, ErrorRule class ExceptionA(Exception): """My testing exception A.""" pass class ExceptionA1(ExceptionA): """My testing exception A1.""" pass class ExceptionA2(ExceptionA): """My testing exception A2.""" pass class ExceptionB(Exception): """My testing exception B.""" pass class ExceptionC(Exception): """My testing exception C.""" pass class CustomRule(ErrorRule): """My custom rule for subclasses.""" def match_type(self, exception_type): return issubclass(exception_type, self._exception_type) class DBusErrorTestCase(unittest.TestCase): """Test the DBus error register and handler.""" def setUp(self): self.error_mapper = ErrorMapper() def _check_type(self, error_name, expected_type): exception_type = self.error_mapper.get_exception_type(error_name) self.assertEqual(exception_type, expected_type) def _check_name(self, exception_type, expected_name): error_name = self.error_mapper.get_error_name(exception_type) self.assertEqual(error_name, expected_name) def test_decorators(self): """Test the error decorators.""" dbus_error = get_error_decorator(self.error_mapper) @dbus_error("org.test.ErrorA") class DecoratedA(Exception): pass @dbus_error("ErrorB", namespace=("org", "test")) class DecoratedB(Exception): pass self._check_name(DecoratedA, "org.test.ErrorA") self._check_type("org.test.ErrorA", DecoratedA) self._check_name(DecoratedB, "org.test.ErrorB") self._check_type("org.test.ErrorB", DecoratedB) def test_simple_rule(self): """Test a simple rule.""" self.error_mapper.add_rule(ErrorRule( exception_type=ExceptionA, error_name="org.test.ErrorA" )) self._check_name(ExceptionA, "org.test.ErrorA") self._check_name(ExceptionA1, "not.known.Error.ExceptionA1") self._check_name(ExceptionA2, "not.known.Error.ExceptionA2") self._check_type("org.test.ErrorA", ExceptionA) self._check_type("org.test.ErrorA1", DBusError) self._check_type("org.test.ErrorA2", DBusError) self._check_name(ExceptionB, "not.known.Error.ExceptionB") self._check_type("org.test.ErrorB", DBusError) def test_custom_rule(self): """Test a custom rule.""" self.error_mapper.add_rule(CustomRule( exception_type=ExceptionA, error_name="org.test.ErrorA" )) self._check_name(ExceptionA, "org.test.ErrorA") self._check_name(ExceptionA1, "org.test.ErrorA") self._check_name(ExceptionA2, "org.test.ErrorA") self._check_type("org.test.ErrorA", ExceptionA) self._check_type("org.test.ErrorA1", DBusError) self._check_type("org.test.ErrorA2", DBusError) self._check_name(ExceptionB, "not.known.Error.ExceptionB") self._check_type("org.test.ErrorB", DBusError) def test_several_rules(self): """Test several rules.""" self.error_mapper.add_rule(ErrorRule( exception_type=ExceptionA, error_name="org.test.ErrorA" )) self.error_mapper.add_rule(ErrorRule( exception_type=ExceptionB, error_name="org.test.ErrorB" )) self._check_name(ExceptionA, "org.test.ErrorA") self._check_name(ExceptionB, "org.test.ErrorB") self._check_name(ExceptionC, "not.known.Error.ExceptionC") self._check_type("org.test.ErrorA", ExceptionA) self._check_type("org.test.ErrorB", ExceptionB) self._check_type("org.test.ErrorC", DBusError) def test_rule_priorities(self): """Test the priorities of the rules.""" self.error_mapper.add_rule(ErrorRule( exception_type=ExceptionA, error_name="org.test.ErrorA1" )) self._check_name(ExceptionA, "org.test.ErrorA1") self._check_type("org.test.ErrorA1", ExceptionA) self._check_type("org.test.ErrorA2", DBusError) self.error_mapper.add_rule(ErrorRule( exception_type=ExceptionA, error_name="org.test.ErrorA2" )) self._check_name(ExceptionA, "org.test.ErrorA2") self._check_type("org.test.ErrorA1", ExceptionA) self._check_type("org.test.ErrorA2", ExceptionA) def test_default_mapping(self): """Test the default error mapping.""" self._check_name(ExceptionA, "not.known.Error.ExceptionA") self._check_type("org.test.ErrorB", DBusError) self._check_type("org.test.ErrorC", DBusError) def test_default_class(self): """Test the default class.""" self._check_type("org.test.ErrorA", DBusError) def test_default_namespace(self): """Test the default namespace.""" self._check_name(ExceptionA, "not.known.Error.ExceptionA") def test_failed_mapping(self): """Test the failed mapping.""" self.error_mapper._error_rules = [] with self.assertRaises(LookupError) as cm: self.error_mapper.get_error_name(ExceptionA) self.assertEqual( "No name found for 'ExceptionA'.", str(cm.exception) ) with self.assertRaises(LookupError) as cm: self.error_mapper.get_exception_type("org.test.ErrorA") self.assertEqual( "No type found for 'org.test.ErrorA'.", str(cm.exception) ) dasbus-1.7/tests/test_identifier.py000066400000000000000000000174141433220137200175430ustar00rootroot00000000000000# # Copyright (C) 2019 Red Hat, Inc. All rights reserved. # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 # USA # import unittest from unittest.mock import Mock from dasbus.identifier import DBusInterfaceIdentifier, DBusObjectIdentifier, \ DBusServiceIdentifier, DBusBaseIdentifier class DBusIdentifierTestCase(unittest.TestCase): """Test DBus identifiers.""" def assert_namespace(self, obj, namespace): """Check the DBus namespace object.""" self.assertEqual(obj.namespace, namespace) def assert_interface(self, obj, interface_name): """Check the DBus interface object.""" self.assertEqual(obj.interface_name, interface_name) def test_identifier(self): """Test the DBus identifier object.""" identifier = DBusBaseIdentifier( namespace=("a", "b", "c") ) self.assert_namespace(identifier, ("a", "b", "c")) identifier = DBusBaseIdentifier( basename="d", namespace=("a", "b", "c") ) self.assert_namespace(identifier, ("a", "b", "c", "d")) def test_interface(self): """Test the DBus interface object.""" interface = DBusInterfaceIdentifier( namespace=("a", "b", "c") ) self.assert_namespace(interface, ("a", "b", "c")) self.assert_interface(interface, "a.b.c") interface = DBusInterfaceIdentifier( namespace=("a", "b", "c"), interface_version=1 ) self.assert_namespace(interface, ("a", "b", "c")) self.assert_interface(interface, "a.b.c1") interface = DBusInterfaceIdentifier( basename="d", namespace=("a", "b", "c"), interface_version=1 ) self.assert_namespace(interface, ("a", "b", "c", "d")) self.assert_interface(interface, "a.b.c.d1") def assert_object(self, obj, object_path): """Check the DBus object.""" self.assertEqual(obj.object_path, object_path) def test_object(self): """Test the DBus object.""" obj = DBusObjectIdentifier( namespace=("a", "b", "c") ) self.assert_namespace(obj, ("a", "b", "c")) self.assert_interface(obj, "a.b.c") self.assert_object(obj, "/a/b/c") obj = DBusObjectIdentifier( namespace=("a", "b", "c"), object_version=2, interface_version=4 ) self.assert_namespace(obj, ("a", "b", "c")) self.assert_interface(obj, "a.b.c4") self.assert_object(obj, "/a/b/c2") obj = DBusObjectIdentifier( basename="d", namespace=("a", "b", "c"), object_version=2, interface_version=4 ) self.assert_namespace(obj, ("a", "b", "c", "d")) self.assert_interface(obj, "a.b.c.d4") self.assert_object(obj, "/a/b/c/d2") def assert_bus(self, obj, message_bus): """Check the DBus service object.""" self.assertEqual(obj.message_bus, message_bus) def assert_service(self, obj, service_name): """Check the DBus service object.""" self.assertEqual(obj.service_name, service_name) def test_service(self): """Test the DBus service object.""" bus = Mock() service = DBusServiceIdentifier( namespace=("a", "b", "c"), message_bus=bus ) self.assert_namespace(service, ("a", "b", "c")) self.assert_interface(service, "a.b.c") self.assert_object(service, "/a/b/c") self.assert_service(service, "a.b.c") self.assert_bus(service, bus) service = DBusServiceIdentifier( namespace=("a", "b", "c"), service_version=3, interface_version=5, object_version=7, message_bus=bus ) self.assert_namespace(service, ("a", "b", "c")) self.assert_interface(service, "a.b.c5") self.assert_object(service, "/a/b/c7") self.assert_service(service, "a.b.c3") self.assert_bus(service, bus) service = DBusServiceIdentifier( basename="d", namespace=("a", "b", "c"), service_version=3, interface_version=5, object_version=7, message_bus=bus ) self.assert_namespace(service, ("a", "b", "c", "d")) self.assert_interface(service, "a.b.c.d5") self.assert_object(service, "/a/b/c/d7") self.assert_service(service, "a.b.c.d3") self.assert_bus(service, bus) class DBusServiceIdentifierTestCase(unittest.TestCase): """Test DBus service identifiers.""" def test_get_proxy(self): """Test getting a proxy.""" bus = Mock() namespace = ("a", "b", "c") service = DBusServiceIdentifier( namespace=namespace, message_bus=bus ) obj = DBusObjectIdentifier( basename="object", namespace=namespace ) service.get_proxy() bus.get_proxy.assert_called_with( "a.b.c", "/a/b/c", None ) bus.reset_mock() service.get_proxy("/a/b/c/object") bus.get_proxy.assert_called_with( "a.b.c", "/a/b/c/object", None ) bus.reset_mock() service.get_proxy(obj) bus.get_proxy.assert_called_with( "a.b.c", "/a/b/c/object", None ) bus.reset_mock() def test_get_proxy_for_interface(self): """Test getting a proxy for an interface.""" bus = Mock() namespace = ("a", "b", "c") service = DBusServiceIdentifier( namespace=namespace, message_bus=bus ) interface = DBusInterfaceIdentifier( basename="interface", namespace=namespace ) service.get_proxy( interface_name="a.b.c.interface" ) bus.get_proxy.assert_called_with( "a.b.c", "/a/b/c", "a.b.c.interface" ) bus.reset_mock() service.get_proxy( interface_name=interface ) bus.get_proxy.assert_called_with( "a.b.c", "/a/b/c", "a.b.c.interface" ) bus.reset_mock() def test_get_proxy_with_bus_arguments(self): """Test getting a proxy with an additional arguments.""" bus = Mock() error_mapper = Mock() namespace = ("a", "b", "c") service = DBusServiceIdentifier( namespace=namespace, message_bus=bus ) service.get_proxy( error_mapper=error_mapper ) bus.get_proxy.assert_called_with( "a.b.c", "/a/b/c", None, error_mapper=error_mapper ) bus.reset_mock() service.get_proxy( interface_name=service, error_mapper=error_mapper ) bus.get_proxy.assert_called_with( "a.b.c", "/a/b/c", "a.b.c", error_mapper=error_mapper ) bus.reset_mock() dasbus-1.7/tests/test_interface.py000066400000000000000000000662001433220137200173560ustar00rootroot00000000000000# # Copyright (C) 2019 Red Hat, Inc. All rights reserved. # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 # USA # import unittest from dasbus.typing import Int, Str, List, Double, UnixFD, Tuple, Bool, Dict from dasbus.server.interface import dbus_interface, dbus_class, dbus_signal, \ get_xml, DBusSpecificationGenerator, DBusSpecificationError, \ accepts_additional_arguments, returns_multiple_arguments from dasbus.xml import XMLGenerator class InterfaceGeneratorTestCase(unittest.TestCase): def setUp(self): self.generator = DBusSpecificationGenerator self.maxDiff = None def _compare(self, cls, expected_xml): """Compare cls specification with the given xml.""" generated_xml = get_xml(cls) # pylint: disable=no-member self.assertEqual( XMLGenerator.prettify_xml(generated_xml), XMLGenerator.prettify_xml(expected_xml) ) def test_exportable(self): """Test if the given name should be exported.""" self.assertTrue(self.generator._is_exportable("Name")) self.assertTrue(self.generator._is_exportable("ValidName")) self.assertTrue(self.generator._is_exportable("ValidName123")) self.assertTrue(self.generator._is_exportable("Valid123Name456")) self.assertTrue(self.generator._is_exportable("VALIDNAME123")) self.assertFalse(self.generator._is_exportable("name")) self.assertFalse(self.generator._is_exportable("_Name")) self.assertFalse(self.generator._is_exportable("__Name")) self.assertFalse(self.generator._is_exportable("invalid_name")) self.assertFalse(self.generator._is_exportable("invalid_name_123")) self.assertFalse(self.generator._is_exportable("Invalid_Name")) self.assertFalse(self.generator._is_exportable("invalidname")) self.assertFalse(self.generator._is_exportable("invalid123")) self.assertFalse(self.generator._is_exportable("invalidName")) self.assertFalse(self.generator._is_exportable("123InvalidName")) def test_invalid_member(self): """Test invalid members.""" class InvalidMemberClass(object): Test1 = None with self.assertRaises(DBusSpecificationError) as cm: self.generator._generate_interface( InvalidMemberClass, {}, "my.example.Interface" ) self.assertEqual( "Unsupported definition of DBus member 'Test1'.", str(cm.exception) ) def test_is_method(self): """Test if methods are DBus methods.""" class IsMethodClass(object): def Test1(self, a: Int): pass # pragma: no cover Test2 = None @property def Test3(self): return None # pragma: no cover @dbus_signal def Test4(self): pass # pragma: no cover self.assertTrue(self.generator._is_method(IsMethodClass.Test1)) self.assertFalse(self.generator._is_method(IsMethodClass.Test2)) self.assertFalse(self.generator._is_method(IsMethodClass.Test3)) self.assertFalse(self.generator._is_method(IsMethodClass.Test4)) def test_invalid_method(self): """Test invalid methods.""" class InvalidMethodClass(object): def Test1(self, a): pass # pragma: no cover def Test2(self, a=None): pass # pragma: no cover def Test3(self, a, b): pass # pragma: no cover def Test4(self, a: Int, b: Str, c): pass # pragma: no cover def Test5(self, a: Int, b: Str, c) -> Int: pass # pragma: no cover def Test6(self, *arg): pass # pragma: no cover def Test7(self, **kwargs): pass # pragma: no cover def Test8(self, a: Int, b: Double, *, c, d=None): pass # pragma: no cover self._check_invalid_method( InvalidMethodClass.Test1, "Undefined type of parameter 'a'." ) self._check_invalid_method( InvalidMethodClass.Test2, "Undefined type of parameter 'a'." ) self._check_invalid_method( InvalidMethodClass.Test3, "Undefined type of parameter 'a'." ) self._check_invalid_method( InvalidMethodClass.Test4, "Undefined type of parameter 'c'." ) self._check_invalid_method( InvalidMethodClass.Test5, "Undefined type of parameter 'c'." ) self._check_invalid_method( InvalidMethodClass.Test6, "Only positional or keyword arguments are allowed." ) self._check_invalid_method( InvalidMethodClass.Test7, "Only positional or keyword arguments are allowed." ) self._check_invalid_method( InvalidMethodClass.Test8, "Only positional or keyword arguments are allowed." ) def _check_invalid_method(self, method, message): """Try to generate a specification for an invalid method.""" with self.assertRaises(DBusSpecificationError) as cm: self.generator._generate_method(method, method.__name__) self.assertEqual(str(cm.exception), message) def test_method(self): """Test interface with a method.""" @dbus_interface("my.example.Interface") class MethodClass(object): def Method1(self): pass # pragma: no cover def Method2(self, x: Int): pass # pragma: no cover def Method3(self) -> Int: pass # pragma: no cover def Method4(self, x: List[Double], y: UnixFD) -> Tuple[Int, Bool]: return 0, True # pragma: no cover expected_xml = ''' ''' self._compare(MethodClass, expected_xml) def test_is_property(self): """Test property detection.""" class IsPropertyClass(object): @property def Property1(self) -> Int: return 1 # pragma: no cover def _get_property2(self): return 2 # pragma: no cover Property2 = property(fget=_get_property2) def Property3(self) -> Int: return 3 # pragma: no cover @dbus_signal def Property4(self): pass # pragma: no cover self.assertTrue(self.generator._is_property( IsPropertyClass.Property1 )) self.assertTrue(self.generator._is_property( IsPropertyClass.Property2 )) self.assertFalse(self.generator._is_property( IsPropertyClass.Property3 )) self.assertFalse(self.generator._is_property( IsPropertyClass.Property4 )) def test_property(self): """Test the interface with a property.""" @dbus_interface("Interface", namespace=("my", "example")) class ReadWritePropertyClass(object): def __init__(self): self._property = 0 # pragma: no cover @property def Property(self) -> Int: return self._property # pragma: no cover @Property.setter def Property(self, value: Int): self._property = value # pragma: no cover expected_xml = ''' ''' self._compare(ReadWritePropertyClass, expected_xml) def test_invalid_property(self): """Test the interface with an invalid property.""" class InvalidPropertyClass(object): InvalidProperty = property() @property def NoHintProperty(self): return 1 # pragma: no cover with self.assertRaises(DBusSpecificationError) as cm: self.generator._generate_property( InvalidPropertyClass.InvalidProperty, "InvalidProperty" ) self.assertEqual( "DBus property 'InvalidProperty' is not accessible.", str(cm.exception) ) with self.assertRaises(DBusSpecificationError) as cm: self.generator._generate_property( InvalidPropertyClass.NoHintProperty, "NoHintProperty" ) self.assertEqual( "Undefined type of DBus property 'NoHintProperty'.", str(cm.exception) ) def test_property_readonly(self): """Test readonly property.""" @dbus_interface("Interface") class ReadonlyPropertyClass(object): def __init__(self): self._property = 0 # pragma: no cover @property def Property(self) -> Int: return self._property # pragma: no cover expected_xml = ''' ''' self._compare(ReadonlyPropertyClass, expected_xml) def test_property_writeonly(self): """Test writeonly property.""" @dbus_interface("Interface") class WriteonlyPropertyClass(object): def __init__(self): self._property = 0 # pragma: no cover def set_property(self, x: Int): self._property = x # pragma: no cover Property = property(fset=set_property) expected_xml = ''' ''' self._compare(WriteonlyPropertyClass, expected_xml) def test_is_signal(self): """Test signal detection.""" class IsSignalClass(object): @dbus_signal def Signal1(self): pass # pragma: no cover @dbus_signal def Signal2(self, x: Int, y: Double): pass # pragma: no cover Signal3 = dbus_signal() def Signal4(self): pass # pragma: no cover @property def Signal5(self): return None # pragma: no cover Signal6 = None self.assertTrue(self.generator._is_signal(IsSignalClass.Signal1)) self.assertTrue(self.generator._is_signal(IsSignalClass.Signal2)) self.assertTrue(self.generator._is_signal(IsSignalClass.Signal3)) self.assertFalse(self.generator._is_signal(IsSignalClass.Signal4)) self.assertFalse(self.generator._is_signal(IsSignalClass.Signal5)) self.assertFalse(self.generator._is_signal(IsSignalClass.Signal6)) def test_simple_signal(self): """Test interface with a simple signal.""" @dbus_interface("Interface") class SimpleSignalClass(object): SimpleSignal = dbus_signal() expected_xml = ''' ''' self._compare(SimpleSignalClass, expected_xml) def test_signal(self): """Test interface with signals.""" @dbus_interface("Interface") class SignalClass(object): @dbus_signal def SomethingHappened(self): """Signal that something happened.""" pass # pragma: no cover @dbus_signal def SignalSomething(self, x: Int, y: Str): """ Signal that something happened. :param x: Parameter x. :param y: Parameter y """ pass # pragma: no cover def _emit_signals(self): # pragma: no cover self.SomethingHappened.emit() # pylint: disable=no-member self.SignalSomething.emit(0, "Something!") expected_xml = ''' ''' self._compare(SignalClass, expected_xml) def test_invalid_signal(self): """Test interface with an invalid signal.""" class InvalidSignalClass(object): @dbus_signal def Signal1(self, x): pass # pragma: no cover @dbus_signal def Signal2(self) -> Int: return 1 # pragma: no cover with self.assertRaises(DBusSpecificationError) as cm: self.generator._generate_signal( InvalidSignalClass.Signal1, "Signal1" ) self.assertEqual( "Undefined type of parameter 'x'.", str(cm.exception) ) with self.assertRaises(DBusSpecificationError) as cm: self.generator._generate_signal( InvalidSignalClass.Signal2, "Signal2" ) self.assertEqual( "Invalid return type of DBus signal 'Signal2'.", str(cm.exception) ) def test_override_method(self): """Test interface with overridden methods.""" @dbus_interface("A") class AClass(object): def MethodA1(self, x: Int) -> Double: return x + 1.0 # pragma: no cover def MethodA2(self) -> Bool: return False # pragma: no cover def MethodA3(self, x: List[Int]): pass # pragma: no cover class BClass(AClass): def MethodA1(self, x: Int) -> Double: return x + 2.0 # pragma: no cover def MethodB1(self) -> Str: return "" # pragma: no cover @dbus_interface("C") class CClass(object): def MethodC1(self) -> Tuple[Int, Int]: return 1, 2 # pragma: no cover def MethodC2(self, x: Double): pass # pragma: no cover @dbus_interface("D") class DClass(BClass, CClass): def MethodD1(self) -> Tuple[Int, Str]: return 0, "" # pragma: no cover def MethodA3(self, x: List[Int]): pass # pragma: no cover def MethodC2(self, x: Double): pass # pragma: no cover expected_xml = ''' ''' self._compare(DClass, expected_xml) def test_complex(self): """Test complex example.""" @dbus_interface("ComplexA") class ComplexClassA(object): def __init__(self): self.a = 1 # pragma: no cover @dbus_signal def SignalA(self, x: Int): pass # pragma: no cover @property def PropertyA(self) -> Double: return self.a + 2.5 # pragma: no cover def MethodA(self) -> Int: return self.a # pragma: no cover def _methodA(self): return None # pragma: no cover expected_xml = ''' ''' self._compare(ComplexClassA, expected_xml) @dbus_interface("ComplexB") class ComplexClassB(ComplexClassA): def __init__(self): # pragma: no cover super().__init__() self.b = 2.0 @dbus_signal def SignalB(self, x: Bool, y: Double, z: Tuple[Int, Int]): pass # pragma: no cover @property def PropertyB(self) -> Double: return self.b # pragma: no cover def MethodA(self) -> Int: return int(self.b) # pragma: no cover def MethodB(self, a: Str, b: List[Double], c: Int) -> Int: return int(self.b) # pragma: no cover def _methodB(self, x: Bool): pass # pragma: no cover expected_xml = ''' ''' self._compare(ComplexClassB, expected_xml) @dbus_class class ComplexClassC(ComplexClassB): def MethodB(self, a: Str, b: List[Double], c: Int) -> Int: return 1 # pragma: no cover expected_xml = ''' ''' self._compare(ComplexClassC, expected_xml) def test_standard_interfaces(self): """Test members of standard interfaces.""" @dbus_interface("InterfaceWithoutStandard") class ClassWithStandard(object): @property def Property(self) -> Int: return 1 # pragma: no cover def Method(self): pass # pragma: no cover def Ping(self): # This method shouldn't be part of the interface pass # pragma: no cover @dbus_signal def PropertiesChanged(self, a, b, c): # This signal shouldn't be part of the interface pass # pragma: no cover expected_xml = ''' ''' self._compare(ClassWithStandard, expected_xml) def test_additional_arguments(self): """Test interface methods with additional arguments.""" @dbus_interface("my.example.Interface") class AdditionalArgumentsClass(object): @accepts_additional_arguments def Method1(self, **info): pass # pragma: no cover @accepts_additional_arguments def Method2(self, x: Int, **info): pass # pragma: no cover @accepts_additional_arguments def Method3(self, *, call_info): pass # pragma: no cover @accepts_additional_arguments def Method4(self, x: Int, *, call_info): pass # pragma: no cover expected_xml = ''' ''' self._compare(AdditionalArgumentsClass, expected_xml) def test_multiple_output_arguments(self): """Test interface methods with multiple output arguments.""" @dbus_interface("my.example.Interface") class MultipleOutputArgumentsClass(object): @returns_multiple_arguments def Method1(self): pass # pragma: no cover @returns_multiple_arguments def Method2(self) -> None: pass # pragma: no cover @returns_multiple_arguments def Method3(self) -> Tuple[Int, Bool, Str]: pass # pragma: no cover @returns_multiple_arguments def Method4(self) -> Tuple[Tuple[Int], List[Str]]: pass # pragma: no cover expected_xml = ''' ''' self._compare(MultipleOutputArgumentsClass, expected_xml) def test_invalid_multiple_output_arguments(self): """Test interface methods with invalid output arguments.""" class InvalidOutputArgumentsClass(object): @returns_multiple_arguments def Method1(self) -> Int: pass # pragma: no cover @returns_multiple_arguments def Method2(self) -> List[Bool]: pass # pragma: no cover @returns_multiple_arguments def Method3(self) -> Dict[Str, Bool]: pass # pragma: no cover @returns_multiple_arguments def Method4(self) -> Tuple[Int]: pass # pragma: no cover self._check_invalid_method( InvalidOutputArgumentsClass.Method1, "Expected a tuple of multiple arguments." ) self._check_invalid_method( InvalidOutputArgumentsClass.Method2, "Expected a tuple of multiple arguments." ) self._check_invalid_method( InvalidOutputArgumentsClass.Method3, "Expected a tuple of multiple arguments." ) self._check_invalid_method( InvalidOutputArgumentsClass.Method4, "Expected a tuple of more than one argument." ) dasbus-1.7/tests/test_namespace.py000066400000000000000000000041441433220137200173510ustar00rootroot00000000000000# # Copyright (C) 2019 Red Hat, Inc. All rights reserved. # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 # USA # import unittest from dasbus.namespace import get_dbus_name, get_dbus_path, \ get_namespace_from_name class DBusNamespaceTestCase(unittest.TestCase): def test_dbus_name(self): """Test DBus path.""" self.assertEqual(get_dbus_name(), "") self.assertEqual(get_dbus_name("a"), "a") self.assertEqual(get_dbus_name("a", "b"), "a.b") self.assertEqual(get_dbus_name("a", "b", "c"), "a.b.c") self.assertEqual(get_dbus_name("org", "freedesktop", "DBus"), "org.freedesktop.DBus") def test_dbus_path(self): """Test DBus path.""" self.assertEqual(get_dbus_path(), "/") self.assertEqual(get_dbus_path("a"), "/a") self.assertEqual(get_dbus_path("a", "b"), "/a/b") self.assertEqual(get_dbus_path("a", "b", "c"), "/a/b/c") self.assertEqual(get_dbus_path("org", "freedesktop", "DBus"), "/org/freedesktop/DBus") def test_namespace(self): """Test namespaces.""" self.assertEqual(get_namespace_from_name("a"), ("a",)) self.assertEqual(get_namespace_from_name("a.b"), ("a", "b")) self.assertEqual(get_namespace_from_name("a.b.c"), ("a", "b", "c")) self.assertEqual(get_namespace_from_name("org.freedesktop.DBus"), ("org", "freedesktop", "DBus")) dasbus-1.7/tests/test_observer.py000066400000000000000000000111461433220137200172440ustar00rootroot00000000000000# # Copyright (C) 2019 Red Hat, Inc. All rights reserved. # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 # USA # import unittest from unittest.mock import patch, Mock from dasbus.constants import DBUS_FLAG_NONE from dasbus.client.observer import DBusObserver class DBusObserverTestCase(unittest.TestCase): """Test DBus observers.""" def _setup_observer(self, observer): """Set up the observer.""" observer._service_available = Mock() observer._service_unavailable = Mock() self.assertFalse(observer.is_service_available) def _make_service_available(self, observer): """Make the service available.""" observer._service_name_appeared_callback() self._test_if_service_available(observer) def _test_if_service_available(self, observer): """Test if service is available.""" self.assertTrue(observer.is_service_available) observer._service_available.emit.assert_called_once_with(observer) observer._service_available.reset_mock() observer._service_unavailable.emit.assert_not_called() observer._service_unavailable.reset_mock() def _make_service_unavailable(self, observer): """Make the service unavailable.""" observer._service_name_vanished_callback() self._test_if_service_unavailable(observer) def _test_if_service_unavailable(self, observer): """Test if service is unavailable.""" self.assertFalse(observer.is_service_available) observer._service_unavailable.emit.assert_called_once_with(observer) observer._service_unavailable.reset_mock() observer._service_available.emit.assert_not_called() observer._service_available.reset_mock() def test_observer(self): """Test the observer.""" observer = DBusObserver(Mock(), "SERVICE") self._setup_observer(observer) self._make_service_available(observer) self._make_service_unavailable(observer) @patch("dasbus.client.observer.Gio") def test_connect(self, gio): """Test Gio support for watching names.""" dbus = Mock() observer = DBusObserver(dbus, "my.service") self._setup_observer(observer) # Connect the observer. observer.connect_once_available() # Check the call. gio.bus_watch_name_on_connection.assert_called_once() args, kwargs = gio.bus_watch_name_on_connection.call_args self.assertEqual(len(args), 5) self.assertEqual(len(kwargs), 0) self.assertEqual(args[0], dbus.connection) self.assertEqual(args[1], "my.service") self.assertEqual(args[2], DBUS_FLAG_NONE) name_appeared_closure = args[3] self.assertTrue(callable(name_appeared_closure)) name_vanished_closure = args[4] self.assertTrue(callable(name_vanished_closure)) # Check the subscription. subscription_id = gio.bus_watch_name_on_connection.return_value self.assertEqual(len(observer._subscriptions), 1) # Check the observer. self.assertFalse(observer.is_service_available) observer._service_available.emit.assert_not_called() observer._service_unavailable.emit.assert_not_called() # Call the name appeared closure. name_appeared_closure(dbus.connection, "my.service", "name.owner") self._test_if_service_available(observer) # Call the name vanished closure. name_vanished_closure(dbus.connection, "my.service") self._test_if_service_unavailable(observer) # Call the name appeared closure again. name_appeared_closure(dbus.connection, "my.service", "name.owner") self._test_if_service_available(observer) # Disconnect the observer. observer.disconnect() gio.bus_unwatch_name.assert_called_once_with( subscription_id ) self._test_if_service_unavailable(observer) self.assertEqual(observer._subscriptions, []) dasbus-1.7/tests/test_property.py000066400000000000000000000335301433220137200173020ustar00rootroot00000000000000# # Copyright (C) 2019 Red Hat, Inc. All rights reserved. # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 # USA # import unittest from unittest.mock import Mock, call from dasbus.server.interface import dbus_interface from dasbus.server.property import PropertiesInterface, PropertiesException, \ PropertiesChanges, emits_properties_changed from dasbus.signal import Signal from dasbus.specification import DBusSpecificationError from dasbus.typing import get_variant, Int from dasbus.server.template import InterfaceTemplate class DBusPropertySpecificationTestCase(unittest.TestCase): """Test generating properties from DBus specification.""" def test_properties_mapping(self): """Test properties mapping.""" class Interface(object): __dbus_xml__ = ''' ''' changes = PropertiesChanges(Interface()) mapping = { member.name: member.interface_name for member in changes._properties_specs.values() } expected_mapping = { "A1": "A", "B1": "B", "C1": "C", "A2": "A", "B2": "B", "C2": "C", "A3": "A", "B3": "B", "C3": "C", } self.assertDictEqual(mapping, expected_mapping) def test_invalid_properties_mapping(self): """Test for invalid properties.""" class Interface(object): __dbus_xml__ = ''' ''' with self.assertRaises(DBusSpecificationError) as cm: PropertiesChanges(Interface()) self.assertEqual( "DBus property 'A1' is defined in more than one interface.", str(cm.exception) ) class DBusPropertyTestCase(unittest.TestCase): """Test support for DBus properties.""" @dbus_interface("I1") class Test1(PropertiesInterface): @property def A(self) -> Int: return 1 @property def B(self) -> Int: return 2 def test_report_changed(self): """Test reporting changed properties.""" test1 = self.Test1() callback = Mock() test1.PropertiesChanged.connect(callback) test1.flush_changes() callback.assert_not_called() test1.report_changed_property("A") test1.flush_changes() callback.assert_called_once_with("I1", { "A": get_variant(Int, 1) }, []) callback.reset_mock() test1.report_changed_property("B") test1.flush_changes() callback.assert_called_once_with("I1", { "B": get_variant(Int, 2) }, []) callback.reset_mock() test1.report_changed_property("B") test1.report_changed_property("A") test1.flush_changes() callback.assert_called_once_with("I1", { "A": get_variant(Int, 1), "B": get_variant(Int, 2) }, []) callback.reset_mock() @dbus_interface("I2") class Test2(PropertiesInterface): def __init__(self): super().__init__() self._a = 1 @property def a(self): return self._a @a.setter def a(self, a): self._a = a self.report_changed_property("A") @property def A(self) -> Int: return self.a @emits_properties_changed def SetA(self, a: Int): self.a = a @emits_properties_changed def SetADirectly(self, a: Int): self._a = a def SetANoSignal(self, a: Int): self.a = a def DoNothing(self): pass def test_emit_changed(self): """Test emitting changed properties.""" test2 = self.Test2() callback = Mock() test2.PropertiesChanged.connect(callback) self.assertEqual(test2.A, 1) test2.DoNothing() self.assertEqual(test2.A, 1) callback.assert_not_called() test2.SetA(10) self.assertEqual(test2.A, 10) callback.assert_called_once_with("I2", { "A": get_variant(Int, 10) }, []) callback.reset_mock() test2.SetA(1000) self.assertEqual(test2.A, 1000) callback.assert_called_once_with("I2", { "A": get_variant(Int, 1000) }, []) callback.reset_mock() test2.SetADirectly(20) self.assertEqual(test2.A, 20) callback.assert_not_called() callback.reset_mock() test2.flush_changes() callback.assert_not_called() test2.SetANoSignal(200) self.assertEqual(test2.A, 200) callback.assert_not_called() test2.flush_changes() callback.assert_called_once_with("I2", { "A": get_variant(Int, 200) }, []) @dbus_interface("I3") class Test3(InterfaceTemplate): def connect_signals(self): super().connect_signals() self.implementation.module_properties_changed.connect( self.flush_changes ) self.watch_property("A", self.implementation.a_changed) self.watch_property("B", self.implementation.b_changed) @property def A(self) -> Int: return self.implementation.a @emits_properties_changed def SetA(self, a: Int): self.implementation.set_a(a) @property def B(self) -> Int: return self.implementation.b @B.setter @emits_properties_changed def B(self, b: Int): self.implementation.b = b class Test3Implementation(object): def __init__(self): self.module_properties_changed = Signal() self.a_changed = Signal() self.b_changed = Signal() self._a = 1 self._b = 2 @property def a(self): return self._a def set_a(self, a): self._a = a self.a_changed.emit() @property def b(self): return self._b @b.setter def b(self, b): self._b = b self.b_changed.emit() def do_external_changes(self, a, b): self.set_a(a) self.b = b def do_secret_changes(self, a, b): self._a = a self._b = b def test_template(self): """Test the template with support for properties.""" test3implementation = self.Test3Implementation() test3 = self.Test3(test3implementation) callback = Mock() test3.PropertiesChanged.connect(callback) self.assertEqual(test3.A, 1) self.assertEqual(test3.B, 2) test3.SetA(10) self.assertEqual(test3.A, 10) callback.assert_called_once_with("I3", { "A": get_variant(Int, 10) }, []) callback.reset_mock() test3.B = 20 self.assertEqual(test3.B, 20) callback.assert_called_once_with("I3", { "B": get_variant(Int, 20) }, []) callback.reset_mock() test3implementation.do_external_changes(100, 200) self.assertEqual(test3.A, 100) self.assertEqual(test3.B, 200) callback.assert_not_called() test3implementation.module_properties_changed.emit() callback.assert_called_once_with("I3", { "A": get_variant(Int, 100), "B": get_variant(Int, 200) }, []) callback.reset_mock() test3implementation.do_secret_changes(1000, 2000) self.assertEqual(test3.A, 1000) self.assertEqual(test3.B, 2000) callback.assert_not_called() test3implementation.module_properties_changed.emit() callback.assert_not_called() @dbus_interface("I4") class Test4(InterfaceTemplate): def connect_signals(self): super().connect_signals() self.implementation.module_properties_changed.connect( self.flush_changes ) self.watch_property("A", self.implementation.a_changed) @property def A(self) -> Int: return self.implementation.a @emits_properties_changed def SetA(self, a: Int): self.implementation.set_a(a) @emits_properties_changed def DoChanges(self, a: Int, b: Int): self.implementation.do_external_changes(a, b) @dbus_interface("I5") class Test5(Test4): def connect_signals(self): super().connect_signals() self.watch_property("B", self.implementation.b_changed) @property def B(self) -> Int: return self.implementation.b @B.setter @emits_properties_changed def B(self, b: Int): self.implementation.b = b class Test5Implementation(Test3Implementation): pass def test_multiple_interfaces(self): """Test template with multiple inheritance.""" test5implementation = self.Test5Implementation() test5 = self.Test5(test5implementation) callback = Mock() test5.PropertiesChanged.connect(callback) self.assertEqual(test5.A, 1) self.assertEqual(test5.B, 2) test5.SetA(10) self.assertEqual(test5.A, 10) callback.assert_called_once_with("I4", { "A": get_variant(Int, 10) }, []) callback.reset_mock() test5.B = 20 self.assertEqual(test5.B, 20) callback.assert_called_once_with("I5", { "B": get_variant(Int, 20) }, []) callback.reset_mock() test5.DoChanges(1, 2) self.assertEqual(test5.A, 1) self.assertEqual(test5.B, 2) callback.assert_has_calls([ call("I4", {"A": get_variant(Int, 1)}, []), call("I5", {"B": get_variant(Int, 2)}, []) ], any_order=True) callback.reset_mock() test5implementation.do_external_changes(100, 200) self.assertEqual(test5.A, 100) self.assertEqual(test5.B, 200) callback.assert_not_called() test5implementation.module_properties_changed.emit() callback.assert_has_calls([ call("I4", {"A": get_variant(Int, 100)}, []), call("I5", {"B": get_variant(Int, 200)}, []) ], any_order=True) callback.reset_mock() test5implementation.do_secret_changes(1000, 2000) self.assertEqual(test5.A, 1000) self.assertEqual(test5.B, 2000) callback.assert_not_called() test5implementation.module_properties_changed.emit() callback.assert_not_called() class Test6(PropertiesInterface): pass def test_invalid_class(self): """Test the properties interface with invalid class.""" with self.assertRaises(DBusSpecificationError) as cm: self.Test6() self.assertEqual( "XML specification is not defined at '__dbus_xml__'.", str(cm.exception) ) @dbus_interface("I7") class Test7(PropertiesInterface): pass def test_invalid_property(self): """Test the properties interface with invalid property.""" test7 = self.Test7() with self.assertRaises(PropertiesException) as cm: test7.report_changed_property("A") self.assertEqual( "DBus object has no property 'A'.", str(cm.exception) ) @dbus_interface("I8") class Test8(InterfaceTemplate): pass class Test8Implementation(object): pass def test_invalid_property_template(self): """Test the template with invalid property.""" test8implementation = self.Test8Implementation() test8 = self.Test8(test8implementation) signal = Mock() with self.assertRaises(PropertiesException) as cm: test8.watch_property("A", signal) self.assertEqual( "DBus object has no property 'A'.", str(cm.exception) ) signal.connect.assert_not_called() dasbus-1.7/tests/test_proxy.py000066400000000000000000000025421433220137200165760ustar00rootroot00000000000000# # Copyright (C) 2019 Red Hat, Inc. All rights reserved. # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 # USA # import unittest from unittest.mock import Mock from dasbus.client.proxy import ObjectProxy, get_object_path class DBusProxyTestCase(unittest.TestCase): """Test support for object proxies.""" def test_get_object_path(self): """Test get_object_path.""" proxy = ObjectProxy(Mock(), "my.service", "/my/path") self.assertEqual(get_object_path(proxy), "/my/path") with self.assertRaises(TypeError) as cm: get_object_path(None) self.assertEqual( "Invalid type 'NoneType'.", str(cm.exception) ) dasbus-1.7/tests/test_server.py000066400000000000000000000313441433220137200167250ustar00rootroot00000000000000# # Copyright (C) 2019 Red Hat, Inc. All rights reserved. # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 # USA # import unittest from textwrap import dedent from unittest.mock import Mock from dasbus.error import ErrorMapper, ErrorRule from dasbus.server.handler import ServerObjectHandler, GLibServer from dasbus.server.interface import accepts_additional_arguments from dasbus.signal import Signal from dasbus.specification import DBusSpecificationError from dasbus.typing import get_variant class MethodFailedException(Exception): """The method has failed.""" pass class DBusServerTestCase(unittest.TestCase): """Test DBus server support.""" NO_PARAMETERS = get_variant("()", tuple()) def setUp(self): self.message_bus = Mock() self.connection = self.message_bus.connection self.error_mapper = ErrorMapper() self.object = None self.object_path = "/my/path" self.handler = None def _publish_object(self, xml=""): """Publish a mocked object.""" self.object = Mock(__dbus_xml__=dedent(xml)) # Raise AttributeError for default methods. del self.object.Get del self.object.Set del self.object.GetAll # Create object signals. self.object.Signal1 = Signal() self.object.Signal2 = Signal() # Create default object signals. self.object.PropertiesChanged = Signal() self.handler = ServerObjectHandler( self.message_bus, self.object_path, self.object, error_mapper=self.error_mapper ) self.handler.connect_object() def _call_method(self, interface, method, parameters=NO_PARAMETERS, reply=None): invocation = Mock() invocation.get_sender.return_value = ":1.0" GLibServer._object_callback( self.connection, Mock(), self.object_path, interface, method, parameters, invocation, (self.handler._method_callback, ()) ) invocation.return_dbus_error.assert_not_called() invocation.return_value.assert_called_once_with(reply) def _call_method_with_error(self, interface, method, parameters=NO_PARAMETERS, error_name=None, error_message=None): invocation = Mock() with self.assertLogs(level='WARN'): self.handler._method_callback( invocation, interface, method, parameters ) invocation.return_dbus_error.assert_called_once() invocation.return_value.assert_not_called() (name, msg), kwargs = invocation.return_dbus_error.call_args self.assertEqual(kwargs, {}) self.assertEqual(name, error_name, "Unexpected error name.") if error_message is not None: self.assertEqual(msg, error_message, "Unexpected error message.") def test_register(self): """Test the object registration.""" with self.assertRaises(DBusSpecificationError) as cm: self._publish_object("") self.assertEqual( "No DBus interfaces for registration.", str(cm.exception) ) self._publish_object(""" """) self.message_bus.connection.register_object.assert_called() self.handler.disconnect_object() self.message_bus.connection.unregister_object.assert_called() def test_method(self): """Test the method publishing.""" self._publish_object(""" """) self.object.Method2.return_value = None self._call_method( "Interface", "Method2", parameters=get_variant("(i)", (1, )) ) self.object.Method2.assert_called_once_with(1) self.object.Method1.return_value = None self._call_method("Interface", "Method1") self.object.Method1.assert_called_once_with() self.object.Method3.return_value = 0 self._call_method( "Interface", "Method3", reply=get_variant("(i)", (0, )) ) self.object.Method3.assert_called_once_with() self.object.Method4.return_value = (1, True) self._call_method( "Interface", "Method4", parameters=get_variant("(ado)", ([1.2, 2.3], "/my/path")), reply=get_variant("((ib))", ((1, True), )) ) self.object.Method4.assert_called_once_with([1.2, 2.3], "/my/path") self.object.Method5.return_value = (1, True) self._call_method( "Interface", "Method5", reply=get_variant("(ib)", (1, True)) ) self.object.Method5.assert_called_once_with() self._call_method_with_error( "Interface", "MethodInvalid", error_name="not.known.Error.DBusSpecificationError", error_message="DBus specification has no member " "'Interface.MethodInvalid'." ) self.error_mapper.add_rule(ErrorRule( exception_type=MethodFailedException, error_name="MethodFailed" )) self.object.Method1.side_effect = MethodFailedException( "The method has failed." ) self._call_method_with_error( "Interface", "Method1", error_name="MethodFailed", error_message="The method has failed." ) def test_invalid_method_result(self): """Test a method with an invalid result.""" self._publish_object(""" """) self.object.Method.return_value = -1 self._call_method_with_error( "Interface", "Method", error_name="not.known.Error.OverflowError" ) def test_property(self): """Test the property publishing.""" self._publish_object(""" """) self.object.Property1 = 0 self._call_method( "org.freedesktop.DBus.Properties", "Get", parameters=get_variant("(ss)", ( "Interface", "Property1" )), reply=get_variant("(v)", (get_variant("i", 0), )) ) self._call_method( "org.freedesktop.DBus.Properties", "Set", parameters=get_variant("(ssv)", ( "Interface", "Property1", get_variant("i", 1) )), ) self.assertEqual(self.object.Property1, 1) self.object.Property2 = "Hello" self._call_method( "org.freedesktop.DBus.Properties", "Get", parameters=get_variant("(ss)", ( "Interface", "Property2" )), reply=get_variant("(v)", (get_variant("s", "Hello"), )) ) self._call_method_with_error( "org.freedesktop.DBus.Properties", "Set", parameters=get_variant("(ssv)", ( "Interface", "Property2", get_variant("s", "World") )), error_name="not.known.Error.AttributeError", error_message="The property Interface.Property2 " "is not writable." ) self.assertEqual(self.object.Property2, "Hello") self.object.Property3 = True self._call_method_with_error( "org.freedesktop.DBus.Properties", "Get", parameters=get_variant("(ss)", ( "Interface", "Property3" )), error_name="not.known.Error.AttributeError", error_message="The property Interface.Property3 " "is not readable." ) self._call_method( "org.freedesktop.DBus.Properties", "Set", parameters=get_variant("(ssv)", ( "Interface", "Property3", get_variant("b", False) )), ) self.assertEqual(self.object.Property3, False) self._call_method( "org.freedesktop.DBus.Properties", "GetAll", parameters=get_variant("(s)", ("Interface", )), reply=get_variant("(a{sv})", ({ "Property1": get_variant("i", 1), "Property2": get_variant("s", "Hello") }, )) ) self.object.PropertiesChanged( "Interface", {"Property1": get_variant("i", 1)}, ["Property2"] ) self.message_bus.connection.emit_signal.assert_called_once_with( None, self.object_path, "org.freedesktop.DBus.Properties", "PropertiesChanged", get_variant("(sa{sv}as)", ( "Interface", {"Property1": get_variant("i", 1)}, ["Property2"] )) ) def test_signal(self): """Test the signal publishing.""" self._publish_object(""" """) self.object.Signal1() self.message_bus.connection.emit_signal.assert_called_once_with( None, self.object_path, "Interface", "Signal1", None ) self.message_bus.connection.emit_signal.reset_mock() self.object.Signal2(1, "Test") self.message_bus.connection.emit_signal.assert_called_once_with( None, self.object_path, "Interface", "Signal2", get_variant("(is)", (1, "Test")) ) def test_call_info(self): self._publish_object(""" """) accepts_additional_arguments(self.object.Method1) self._call_method("Interface", "Method1") self.object.Method1.assert_called_once_with( call_info={"sender": ":1.0"} ) accepts_additional_arguments(self.object.Method2) self._call_method( "Interface", "Method2", parameters=get_variant("(i)", (1, )) ) self.object.Method2.assert_called_once_with( 1, call_info={"sender": ":1.0"} ) dasbus-1.7/tests/test_signal.py000066400000000000000000000104731433220137200166740ustar00rootroot00000000000000# # Copyright (C) 2019 Red Hat, Inc. All rights reserved. # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 # USA # import unittest from unittest.mock import Mock from dasbus.server.interface import dbus_signal from dasbus.signal import Signal class DBusSignalTestCase(unittest.TestCase): """Test DBus signals.""" def test_create_signal(self): """Create a signal.""" class Interface(object): @dbus_signal def Signal(self): pass interface = Interface() signal = interface.Signal self.assertIsInstance(signal, Signal) self.assertTrue(hasattr(interface, "__dbus_signal_signal")) self.assertEqual(getattr(interface, "__dbus_signal_signal"), signal) def test_emit_signal(self): """Emit a signal.""" class Interface(object): @dbus_signal def Signal(self, a, b, c): pass interface = Interface() signal = interface.Signal callback = Mock() signal.connect(callback) # pylint: disable=no-member signal.emit(1, 2, 3) # pylint: disable=no-member callback.assert_called_once_with(1, 2, 3) callback.reset_mock() signal.emit(4, 5, 6) # pylint: disable=no-member callback.assert_called_once_with(4, 5, 6) callback.reset_mock() def test_disconnect_signal(self): """Disconnect a signal.""" class Interface(object): @dbus_signal def Signal(self): pass interface = Interface() callback = Mock() interface.Signal.connect(callback) # pylint: disable=no-member interface.Signal() callback.assert_called_once_with() callback.reset_mock() interface.Signal.disconnect(callback) # pylint: disable=no-member interface.Signal() callback.assert_not_called() interface.Signal.connect(callback) # pylint: disable=no-member interface.Signal.disconnect() # pylint: disable=no-member interface.Signal() callback.assert_not_called() def test_signals(self): """Test a class with two signals.""" class Interface(object): @dbus_signal def Signal1(self): pass @dbus_signal def Signal2(self): pass interface = Interface() signal1 = interface.Signal1 signal2 = interface.Signal2 self.assertNotEqual(signal1, signal2) callback1 = Mock() signal1.connect(callback1) # pylint: disable=no-member callback2 = Mock() signal2.connect(callback2) # pylint: disable=no-member signal1.emit() # pylint: disable=no-member callback1.assert_called_once_with() callback2.assert_not_called() callback1.reset_mock() signal2.emit() # pylint: disable=no-member callback1.assert_not_called() callback2.assert_called_once_with() def test_instances(self): """Test two instances of the class with a signal.""" class Interface(object): @dbus_signal def Signal(self): pass interface1 = Interface() signal1 = interface1.Signal interface2 = Interface() signal2 = interface2.Signal self.assertNotEqual(signal1, signal2) callback = Mock() signal1.connect(callback) # pylint: disable=no-member callback2 = Mock() signal2.connect(callback2) # pylint: disable=no-member signal1.emit() # pylint: disable=no-member callback.assert_called_once_with() callback2.assert_not_called() dasbus-1.7/tests/test_specification.py000066400000000000000000000342321433220137200202360ustar00rootroot00000000000000# # Copyright (C) 2019 Red Hat, Inc. All rights reserved. # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 # USA # import unittest from dasbus.specification import DBusSpecificationParser, DBusSpecification, \ DBusSpecificationError class SpecificationTestCase(unittest.TestCase): def test_from_members(self): """Test a specification created from members.""" method = DBusSpecification.Method( name="Method", interface_name="A", in_type="()", out_type="(s)" ) signal = DBusSpecification.Signal( name="Signal", interface_name="B", type="(ii)" ) prop = DBusSpecification.Property( name="Property", interface_name="B", readable=True, writable=False, type="i" ) specification = DBusSpecification() specification.add_member(method) specification.add_member(signal) specification.add_member(prop) with self.assertRaises(DBusSpecificationError) as cm: specification.get_member("A", "Invalid") self.assertEqual( "DBus specification has no member 'A.Invalid'.", str(cm.exception) ) with self.assertRaises(DBusSpecificationError) as cm: specification.get_member("Invalid", "Method") self.assertEqual( "DBus specification has no member 'Invalid.Method'.", str(cm.exception) ) self.assertEqual(specification.interfaces, ["A", "B"]) self.assertEqual(specification.members, [method, signal, prop]) self.assertEqual(specification.get_member("A", "Method"), method) self.assertEqual(specification.get_member("B", "Signal"), signal) self.assertEqual(specification.get_member("B", "Property"), prop) def test_from_xml(self): """Test a specification created from XML.""" xml = ''' ''' specification = DBusSpecification.from_xml(xml) self.assertEqual(specification.interfaces, [ 'org.freedesktop.DBus.Introspectable', 'org.freedesktop.DBus.Peer', 'org.freedesktop.DBus.Properties', 'ComplexA', 'ComplexB', ]) self.assertEqual(specification.members, [ DBusSpecification.Method( name='Introspect', interface_name='org.freedesktop.DBus.Introspectable', in_type=None, out_type='(s)' ), DBusSpecification.Method( name='Ping', interface_name='org.freedesktop.DBus.Peer', in_type=None, out_type=None ), DBusSpecification.Method( name='GetMachineId', interface_name='org.freedesktop.DBus.Peer', in_type=None, out_type='(s)' ), DBusSpecification.Method( name='Get', interface_name='org.freedesktop.DBus.Properties', in_type='(ss)', out_type='(v)' ), DBusSpecification.Method( name='GetAll', interface_name='org.freedesktop.DBus.Properties', in_type='(s)', out_type='(a{sv})' ), DBusSpecification.Method( name='Set', interface_name='org.freedesktop.DBus.Properties', in_type='(ssv)', out_type=None ), DBusSpecification.Signal( name='PropertiesChanged', interface_name='org.freedesktop.DBus.Properties', type='(sa{sv}as)' ), DBusSpecification.Method( name='MethodA', interface_name='ComplexA', in_type=None, out_type='(i)' ), DBusSpecification.Property( name='PropertyA', interface_name='ComplexA', readable=True, writable=False, type='d' ), DBusSpecification.Signal( name='SignalA', interface_name='ComplexA', type='(i)' ), DBusSpecification.Method( name='MethodB', interface_name='ComplexB', in_type='(sadi)', out_type='(i)' ), DBusSpecification.Property( name='PropertyB', interface_name='ComplexB', readable=True, writable=False, type='d' ), DBusSpecification.Signal( name='SignalB', interface_name='ComplexB', type='(bd(ii))' ) ]) class SpecificationParserTestCase(unittest.TestCase): def setUp(self): self.maxDiff = None def _compare(self, xml, expected_members): """Compare members of the specification.""" specification = DBusSpecification() DBusSpecificationParser._parse_xml(specification, xml) self.assertEqual(specification.members, expected_members) def test_method(self): """Test XML with methods.""" xml = ''' ''' self._compare(xml, [ DBusSpecification.Method( name='Method1', interface_name='Interface', in_type=None, out_type=None ), DBusSpecification.Method( name='Method2', interface_name='Interface', in_type='(i)', out_type=None ), DBusSpecification.Method( name='Method3', interface_name='Interface', in_type=None, out_type='(i)' ), DBusSpecification.Method( name='Method4', interface_name='Interface', in_type='(adh)', out_type='((ib))' ) ]) def test_property(self): """Test XML with a property.""" xml = ''' ''' self._compare(xml, [ DBusSpecification.Property( name='Property', interface_name='Interface', readable=True, writable=True, type='i' ) ]) def test_property_readonly(self): """Test readonly property.""" xml = ''' ''' self._compare(xml, [ DBusSpecification.Property( name='Property', interface_name='Interface', readable=True, writable=False, type='i' ) ]) def test_property_writeonly(self): """Test writeonly property.""" xml = ''' ''' self._compare(xml, [ DBusSpecification.Property( name='Property', interface_name='Interface', readable=False, writable=True, type='i' ) ]) def test_simple_signal(self): """Test interface with a simple signal.""" xml = ''' ''' self._compare(xml, [ DBusSpecification.Signal( name='SimpleSignal', interface_name='Interface', type=None ) ]) def test_signal(self): """Test interface with signals.""" xml = ''' ''' self._compare(xml, [ DBusSpecification.Signal( name='SignalSomething', interface_name='Interface', type='(is)' ), DBusSpecification.Signal( name='SomethingHappened', interface_name='Interface', type=None ) ]) def test_ignore(self): """Ignore invalid XML elements..""" xml = ''' ''' self._compare(xml, [ DBusSpecification.Method( name='MethodA', interface_name='InterfaceA', in_type=None, out_type='(i)' ), DBusSpecification.Property( name='PropertyA', interface_name='InterfaceA', readable=True, writable=False, type='d' ), DBusSpecification.Signal( name='SignalA', interface_name='InterfaceA', type=None ) ]) self._compare("", []) def test_standard_interfaces(self): """Test with the standard interfaces.""" specification = DBusSpecificationParser.parse_specification('') self.assertEqual(specification.members, [ DBusSpecification.Method( name='Introspect', interface_name='org.freedesktop.DBus.Introspectable', in_type=None, out_type='(s)' ), DBusSpecification.Method( name='Ping', interface_name='org.freedesktop.DBus.Peer', in_type=None, out_type=None ), DBusSpecification.Method( name='GetMachineId', interface_name='org.freedesktop.DBus.Peer', in_type=None, out_type='(s)' ), DBusSpecification.Method( name='Get', interface_name='org.freedesktop.DBus.Properties', in_type='(ss)', out_type='(v)' ), DBusSpecification.Method( name='GetAll', interface_name='org.freedesktop.DBus.Properties', in_type='(s)', out_type='(a{sv})' ), DBusSpecification.Method( name='Set', interface_name='org.freedesktop.DBus.Properties', in_type='(ssv)', out_type=None ), DBusSpecification.Signal( name='PropertiesChanged', interface_name='org.freedesktop.DBus.Properties', type='(sa{sv}as)' ) ]) dasbus-1.7/tests/test_structure.py000066400000000000000000000446251433220137200174650ustar00rootroot00000000000000# # Copyright (C) 2019 Red Hat, Inc. All rights reserved. # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 # USA # import unittest from dasbus.typing import get_variant, get_native, Int, Str, List, Bool, \ Dict, Structure, Variant from dasbus.structure import DBusData, DBusStructureError, compare_data, \ generate_string_from_data class DBusStructureTestCase(unittest.TestCase): """Test the DBus structure support.""" def test_empty_structure(self): with self.assertRaises(DBusStructureError) as cm: class NoData(DBusData): pass NoData() self.assertEqual(str(cm.exception), "No fields found.") def test_readonly_structure(self): with self.assertRaises(DBusStructureError) as cm: class ReadOnlyData(DBusData): @property def x(self) -> Int: return 1 ReadOnlyData() self.assertEqual(str(cm.exception), "Field 'x' cannot be set.") def test_writeonly_structure(self): with self.assertRaises(DBusStructureError) as cm: class WriteOnlyData(DBusData): def __init__(self): self._x = 0 def set_x(self, x): self._x = x x = property(None, set_x) WriteOnlyData() self.assertEqual(str(cm.exception), "Field 'x' cannot be get.") def test_no_type_structure(self): with self.assertRaises(DBusStructureError) as cm: class NoTypeData(DBusData): def __init__(self): self._x = 0 @property def x(self): return self._x @x.setter def x(self, x): self._x = x NoTypeData() self.assertEqual(str(cm.exception), "Field 'x' has unknown type.") class SkipData(DBusData): class_attribute = 1 def __init__(self): self._x = 0 self._y = 1 @property def x(self) -> Int: return self._x @property def _private_property(self): return 1 @x.setter def x(self, x): self._x = x def method(self): pass def test_skip_members(self): data = self.SkipData() structure = self.SkipData.to_structure(data) self.assertEqual(structure, { 'x': get_variant(Int, 0) }) data = self.SkipData.from_structure({ 'x': get_variant(Int, 10) }) self.assertEqual(data.x, 10) class SimpleData(DBusData): def __init__(self): self._x = 0 @property def x(self) -> Int: return self._x @x.setter def x(self, x): self._x = x def test_get_simple_structure(self): data = self.SimpleData() self.assertEqual(data.x, 0) structure = self.SimpleData.to_structure(data) self.assertEqual(structure, {'x': get_variant(Int, 0)}) data.x = 10 self.assertEqual(data.x, 10) structure = self.SimpleData.to_structure(data) self.assertEqual(structure, {'x': get_variant(Int, 10)}) def test_get_simple_structure_list(self): d1 = self.SimpleData() d1.x = 1 d2 = self.SimpleData() d2.x = 2 d3 = self.SimpleData() d3.x = 3 structures = self.SimpleData.to_structure_list([d1, d2, d3]) self.assertEqual(structures, [ {'x': get_variant(Int, 1)}, {'x': get_variant(Int, 2)}, {'x': get_variant(Int, 3)} ]) def test_apply_simple_structure(self): data = self.SimpleData() self.assertEqual(data.x, 0) structure = {'x': get_variant(Int, 10)} data = self.SimpleData.from_structure(structure) self.assertEqual(data.x, 10) def test_apply_simple_invalid_structure(self): with self.assertRaises(DBusStructureError) as cm: self.SimpleData.from_structure({'y': get_variant(Int, 10)}) self.assertEqual(str(cm.exception), "Field 'y' doesn't exist.") def test_apply_simple_structure_list(self): s1 = {'x': get_variant(Int, 1)} s2 = {'x': get_variant(Int, 2)} s3 = {'x': get_variant(Int, 3)} data = self.SimpleData.from_structure_list([s1, s2, s3]) self.assertEqual(len(data), 3) self.assertEqual(data[0].x, 1) self.assertEqual(data[1].x, 2) self.assertEqual(data[2].x, 3) def test_compare_simple_structure(self): data = self.SimpleData() self.assertTrue(compare_data(data, data)) self.assertTrue(compare_data(data, self.SimpleData())) self.assertFalse(compare_data(data, None)) self.assertFalse(compare_data(None, data)) self.assertTrue(compare_data( self.SimpleData.from_structure({'x': get_variant(Int, 10)}), self.SimpleData.from_structure({'x': get_variant(Int, 10)}) )) self.assertFalse(compare_data( self.SimpleData.from_structure({'x': get_variant(Int, 10)}), self.SimpleData.from_structure({'x': get_variant(Int, 9)}) )) class OtherData(DBusData): def __init__(self): self._x = 0 @property def x(self) -> Int: return self._x @x.setter def x(self, x): self._x = x def test_get_structure_from_invalid_type(self): data = self.OtherData() with self.assertRaises(TypeError) as cm: self.SimpleData.to_structure(data) self.assertEqual(str(cm.exception), "Invalid type 'OtherData'.") def test_apply_structure_with_invalid_type(self): structure = ["x"] with self.assertRaises(TypeError) as cm: self.SimpleData.from_structure(structure) self.assertEqual(str(cm.exception), "Invalid type 'list'.") def test_apply_structure_with_invalid_type_variant(self): structure = get_variant(List[Structure], [{'x': get_variant(Int, 10)}]) with self.assertRaises(TypeError) as cm: self.SimpleData.from_structure_list(structure) self.assertEqual(str(cm.exception), "Invalid type 'Variant'.") class ComplicatedData(DBusData): def __init__(self): self._very_long_property_name = "" self._bool_list = [] self._dictionary = {} @property def dictionary(self) -> Dict[Int, Str]: return self._dictionary @dictionary.setter def dictionary(self, value): self._dictionary = value @property def bool_list(self) -> List[Bool]: return self._bool_list @bool_list.setter def bool_list(self, value): self._bool_list = value @property def very_long_property_name(self) -> Str: return self._very_long_property_name @very_long_property_name.setter def very_long_property_name(self, value): self._very_long_property_name = value def test_get_complicated_structure(self): data = self.ComplicatedData() data.dictionary = {1: "1", 2: "2"} data.bool_list = [True, False, False] data.very_long_property_name = "My String Value" self.assertEqual( { 'dictionary': get_variant( Dict[Int, Str], {1: "1", 2: "2"} ), 'bool-list': get_variant( List[Bool], [True, False, False] ), 'very-long-property-name': get_variant( Str, "My String Value" ) }, self.ComplicatedData.to_structure(data) ) def test_apply_complicated_structure(self): data = self.ComplicatedData.from_structure( { 'dictionary': get_variant( Dict[Int, Str], {1: "1", 2: "2"} ), 'bool-list': get_variant( List[Bool], [True, False, False] ), 'very-long-property-name': get_variant( Str, "My String Value" ) } ) self.assertEqual(data.dictionary, {1: "1", 2: "2"}) self.assertEqual(data.bool_list, [True, False, False]) self.assertEqual(data.very_long_property_name, "My String Value") def test_compare_complicated_structure(self): self.assertTrue(compare_data( self.ComplicatedData(), self.ComplicatedData(), )) self.assertFalse(compare_data( self.ComplicatedData(), self.SimpleData() )) self.assertFalse(compare_data( self.SimpleData(), self.ComplicatedData() )) self.assertTrue(compare_data( self.ComplicatedData.from_structure( { 'dictionary': get_variant( Dict[Int, Str], {1: "1", 2: "2"} ), 'bool-list': get_variant( List[Bool], [True, False, False] ), 'very-long-property-name': get_variant( Str, "My String Value" ) } ), self.ComplicatedData.from_structure( { 'dictionary': get_variant( Dict[Int, Str], {1: "1", 2: "2"} ), 'bool-list': get_variant( List[Bool], [True, False, False] ), 'very-long-property-name': get_variant( Str, "My String Value" ) } ) )) self.assertFalse(compare_data( self.ComplicatedData.from_structure( { 'dictionary': get_variant( Dict[Int, Str], {1: "1", 2: "2"} ), 'bool-list': get_variant( List[Bool], [True, False, False] ), 'very-long-property-name': get_variant( Str, "My String Value" ) } ), self.ComplicatedData.from_structure( { 'dictionary': get_variant( Dict[Int, Str], {1: "1", 2: "2"} ), 'bool-list': get_variant( List[Bool], [True, False, True] ), 'very-long-property-name': get_variant( Str, "My String Value" ) } ) )) def test_get_native_complicated_structure(self): data = self.ComplicatedData.from_structure({ 'dictionary': get_variant( Dict[Int, Str], {1: "1", 2: "2"} ), 'bool-list': get_variant( List[Bool], [True, False, False] ), 'very-long-property-name': get_variant( Str, "My String Value" ) }) structure = self.ComplicatedData.to_structure( data ) dictionary = { 'dictionary': {1: "1", 2: "2"}, 'bool-list': [True, False, False], 'very-long-property-name': "My String Value" } self.assertEqual(get_native(structure), dictionary) self.assertEqual(get_native(dictionary), dictionary) class StringData(DBusData): def __init__(self): self._a = 1 self._b = "" self._c = [] self._d = [] @property def a(self) -> Int: return self._a @a.setter def a(self, value): self._a = value @property def b(self) -> Str: return self._b @b.setter def b(self, value): self._b = value @property def c(self) -> List[Bool]: return self._c @c.setter def c(self, value): self._c = value def test_string_representation(self): data = self.StringData() expected = "StringData(a=1, b='', c=[])" self.assertEqual(expected, repr(data)) self.assertEqual(expected, str(data)) data.a = 123 data.b = "HELLO" data.c = [True, False] expected = "StringData(a=123, b='HELLO', c=[True, False])" self.assertEqual(expected, repr(data)) self.assertEqual(expected, str(data)) class AdvancedStringData(DBusData): def __init__(self): self._a = "" self._b = "" self._c = "" @property def a(self) -> Str: return self._a @a.setter def a(self, value): self._a = value @property def b(self) -> Str: return self._b @b.setter def b(self, value): self._b = value @property def c(self) -> Str: return self._c @c.setter def c(self, value): self._c = value def __repr__(self): return generate_string_from_data( obj=self, skip=["b"], add={"b_is_set": bool(self.b)} ) def test_advanced_string_representation(self): data = self.AdvancedStringData() expected = "AdvancedStringData(a='', b_is_set=False, c='')" self.assertEqual(expected, repr(data)) self.assertEqual(expected, str(data)) data.a = "A" data.b = "B" data.c = "C" expected = "AdvancedStringData(a='A', b_is_set=True, c='C')" self.assertEqual(expected, repr(data)) self.assertEqual(expected, str(data)) def test_generate_string_from_invalid_type(self): with self.assertRaises(DBusStructureError) as cm: generate_string_from_data({"x": 1}) self.assertEqual( str(cm.exception), "Fields are not defined at '__dbus_fields__'." ) def test_nested_structure(self): class SimpleData(DBusData): def __init__(self): self._x = 0 @property def x(self) -> Int: return self._x @x.setter def x(self, value): self._x = value class SecretData(DBusData): def __init__(self): self._y = "" @property def y(self) -> Str: return self._y @y.setter def y(self, value): self._y = value def __repr__(self): return generate_string_from_data( self, skip=["y"], add={"y_set": bool(self.y)} ) class NestedData(DBusData): def __init__(self): self._attr = SimpleData() self._secret = SecretData() self._list = [] @property def attr(self) -> SimpleData: return self._attr @attr.setter def attr(self, value): self._attr = value @property def secret(self) -> SecretData: return self._secret @secret.setter def secret(self, value): self._secret = value @property def list(self) -> List[SimpleData]: return self._list @list.setter def list(self, value): self._list = value data = NestedData() expected = \ "NestedData(" \ "attr=SimpleData(x=0), " \ "list=[], " \ "secret=SecretData(y_set=False))" self.assertEqual(str(data), expected) self.assertEqual(repr(data), expected) data.attr.x = -1 data.secret.y = "SECRET" for x in range(2): item = SimpleData() item.x = x data.list.append(item) expected = \ "NestedData(" \ "attr=SimpleData(x=-1), " \ "list=[SimpleData(x=0), SimpleData(x=1)], " \ "secret=SecretData(y_set=True))" self.assertEqual(str(data), expected) self.assertEqual(repr(data), expected) self.assertEqual(NestedData.to_structure(data), { 'attr': get_variant(Structure, { 'x': get_variant(Int, -1) }), 'secret': get_variant(Structure, { 'y': get_variant(Str, "SECRET") }), 'list': get_variant(List[Structure], [ {'x': get_variant(Int, 0)}, {'x': get_variant(Int, 1)} ]) }) dictionary = { 'attr': {'x': 10}, 'secret': {'y': "SECRET"}, 'list': [{'x': 200}, {'x': 300}] } structure = { 'attr': get_variant(Dict[Str, Variant], { 'x': get_variant(Int, 10) }), 'secret': get_variant(Dict[Str, Variant], { 'y': get_variant(Str, "SECRET") }), 'list': get_variant(List[Dict[Str, Variant]], [ {'x': get_variant(Int, 200)}, {'x': get_variant(Int, 300)} ]) } data = NestedData.from_structure(structure) self.assertEqual(data.attr.x, 10) self.assertEqual(data.secret.y, "SECRET") self.assertEqual(len(data.list), 2) self.assertEqual(data.list[0].x, 200) self.assertEqual(data.list[1].x, 300) structure = NestedData.to_structure(data) self.assertEqual(get_native(structure), dictionary) dasbus-1.7/tests/test_typing.py000066400000000000000000000355401433220137200167330ustar00rootroot00000000000000# # Copyright (C) 2019 Red Hat, Inc. All rights reserved. # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 # USA # import unittest from typing import Set from dasbus.typing import get_dbus_type, is_base_type, get_native, \ get_variant, get_variant_type, Int, Int16, Int32, Int64, UInt16, UInt32, \ UInt64, Bool, Byte, Str, Dict, List, Tuple, Variant, Double, ObjPath, \ UnixFD, unwrap_variant, get_type_name, is_tuple_of_one, \ get_type_arguments, VariantUnpacker, VariantUnwrapper import gi gi.require_version("GLib", "2.0") from gi.repository import GLib class DBusTypingTests(unittest.TestCase): def _compare(self, type_hint, expected_string): """Compare generated and expected types.""" # Generate a type string. dbus_type = get_dbus_type(type_hint) self.assertEqual(dbus_type, expected_string) self.assertTrue(GLib.VariantType.string_is_valid(dbus_type)) # Create a variant type from a type hint. variant_type = get_variant_type(type_hint) self.assertIsInstance(variant_type, GLib.VariantType) self.assertEqual(variant_type.dup_string(), expected_string) expected_type = GLib.VariantType.new(expected_string) self.assertTrue(expected_type.equal(variant_type)) # Create a variant type from a type string. variant_type = get_variant_type(expected_string) self.assertIsInstance(variant_type, GLib.VariantType) self.assertTrue(expected_type.equal(variant_type)) # Test the is_tuple_of_one function. expected_value = is_base_type(type_hint, Tuple) \ and len(get_type_arguments(type_hint)) == 1 self.assertEqual(is_tuple_of_one(type_hint), expected_value) self.assertEqual(is_tuple_of_one(expected_string), expected_value) self.assertTrue(is_tuple_of_one(Tuple[type_hint])) self.assertTrue(is_tuple_of_one("({})".format(expected_string))) def test_unknown(self): """Test the unknown type.""" class UnknownType: pass with self.assertRaises(TypeError) as cm: get_dbus_type(UnknownType) self.assertEqual( "Invalid DBus type 'UnknownType'.", str(cm.exception) ) with self.assertRaises(TypeError) as cm: get_dbus_type(List[UnknownType]) self.assertEqual( "Invalid DBus type 'UnknownType'.", str(cm.exception) ) with self.assertRaises(TypeError) as cm: get_dbus_type(Tuple[Int, Str, UnknownType]) self.assertEqual( "Invalid DBus type 'UnknownType'.", str(cm.exception) ) with self.assertRaises(TypeError) as cm: get_dbus_type(Dict[Int, UnknownType]) self.assertEqual( "Invalid DBus type 'UnknownType'.", str(cm.exception) ) def test_invalid(self): """Test the invalid types.""" msg = "Invalid DBus type of dictionary key: '{}'" with self.assertRaises(TypeError) as cm: get_dbus_type(Dict[List[Bool], Bool]) self.assertEqual( msg.format(get_type_name(List[Bool])), str(cm.exception) ) with self.assertRaises(TypeError) as cm: get_dbus_type(Dict[Variant, Int]) self.assertEqual( msg.format("Variant"), str(cm.exception) ) with self.assertRaises(TypeError) as cm: get_dbus_type(Tuple[Int, Double, Dict[Tuple[Int, Int], Bool]]) self.assertEqual( msg.format(get_type_name(Tuple[Int, Int])), str(cm.exception) ) msg = "Invalid DBus type '{}'." with self.assertRaises(TypeError) as cm: get_dbus_type(Set[Int]) self.assertEqual( msg.format(get_type_name(Set[Int])), str(cm.exception), ) def test_simple(self): """Test simple types.""" self._compare(int, "i") self._compare(bool, "b") self._compare(float, "d") self._compare(str, "s") def test_basic(self): """Test basic types.""" self._compare(Int, "i") self._compare(Bool, "b") self._compare(Double, "d") self._compare(Str, "s") self._compare(ObjPath, "o") self._compare(UnixFD, "h") self._compare(Variant, "v") def test_int(self): """Test integer types.""" self._compare(Byte, "y") self._compare(Int16, "n") self._compare(UInt16, "q") self._compare(Int32, "i") self._compare(UInt32, "u") self._compare(Int64, "x") self._compare(UInt64, "t") def test_container(self): """Test container types.""" self._compare(Tuple[Bool], "(b)") self._compare(Tuple[Int, Str], "(is)") self._compare(Tuple[UnixFD, Variant, Double], "(hvd)") self._compare(List[Int], "ai") self._compare(List[Bool], "ab") self._compare(List[UnixFD], "ah") self._compare(List[ObjPath], "ao") self._compare(Dict[Str, Int], "a{si}") self._compare(Dict[Int, Bool], "a{ib}") def test_alias(self): """Test type aliases.""" AliasType = List[Double] self._compare(Dict[Str, AliasType], "a{sad}") def test_depth(self): """Test difficult type structures.""" self._compare(Tuple[Int, Tuple[Str, Str]], "(i(ss))") self._compare(Tuple[Tuple[Tuple[Int]]], "(((i)))") self._compare(Tuple[Bool, Tuple[Tuple[Int], Str]], "(b((i)s))") self._compare(List[List[List[Int]]], "aaai") self._compare(List[Tuple[Dict[Str, Int]]], "a(a{si})") self._compare(List[Dict[Str, Tuple[UnixFD, Variant]]], "aa{s(hv)}") self._compare(Dict[Str, List[Bool]], "a{sab}") self._compare(Dict[Str, Tuple[Int, Int, Double]], "a{s(iid)}") self._compare(Dict[Str, Tuple[Int, Int, Dict[Int, Str]]], "a{s(iia{is})}") def test_base_type(self): """Test the base type checks.""" self.assertEqual(is_base_type(Int, Int), True) self.assertEqual(is_base_type(UInt16, UInt16), True) self.assertEqual(is_base_type(Variant, Variant), True) self.assertEqual(is_base_type(Int, Bool), False) self.assertEqual(is_base_type(Bool, List), False) self.assertEqual(is_base_type(UInt16, Dict), False) self.assertEqual(is_base_type(UInt16, Int), False) self.assertEqual(is_base_type(Variant, Tuple), False) self.assertEqual(is_base_type(List[Int], List), True) self.assertEqual(is_base_type(List[Bool], List), True) self.assertEqual(is_base_type(List[Variant], List), True) self.assertEqual(is_base_type(Tuple[Int], Tuple), True) self.assertEqual(is_base_type(Tuple[Bool], Tuple), True) self.assertEqual(is_base_type(Tuple[Variant], Tuple), True) self.assertEqual(is_base_type(Dict[Str, Int], Dict), True) self.assertEqual(is_base_type(Dict[Str, Bool], Dict), True) self.assertEqual(is_base_type(Dict[Str, Variant], Dict), True) self.assertEqual(is_base_type(List[Int], Tuple), False) self.assertEqual(is_base_type(Tuple[Bool], Dict), False) self.assertEqual(is_base_type(Dict[Str, Variant], List), False) def test_base_class(self): """Test the base class checks.""" class Data(object): pass class DataA(Data): pass class DataB(Data): pass self.assertEqual(is_base_type(Data, Data), True) self.assertEqual(is_base_type(DataA, Data), True) self.assertEqual(is_base_type(DataB, Data), True) self.assertEqual(is_base_type(Data, DataA), False) self.assertEqual(is_base_type(Data, DataB), False) self.assertEqual(is_base_type(DataA, DataB), False) self.assertEqual(is_base_type(DataB, DataA), False) self.assertEqual(is_base_type(List[Data], List), True) self.assertEqual(is_base_type(Tuple[DataA], Tuple), True) self.assertEqual(is_base_type(Dict[Str, DataB], Dict), True) class DBusTypingVariantTests(unittest.TestCase): def _test_variant(self, type_hint, expected_string, value, native_value=None): """Create a variant.""" # The value is native by default. if native_value is None: native_value = value # Create a variant from a type hint. v1 = get_variant(type_hint, value) self.assertTrue(isinstance(v1, Variant)) self.assertEqual(v1.format_string, expected_string) # Check unpacking. self.assertEqual(v1.unpack(), native_value) self.assertEqual(VariantUnpacker.apply(v1), native_value) # Check unwrapping. self.assertEqual(unwrap_variant(v1), value) self.assertEqual(VariantUnwrapper.apply(v1), value) # Create a variant from a type string. v2 = Variant(expected_string, value) self.assertTrue(v2.equal(v1)) self.assertEqual(get_native(v1), native_value) self.assertEqual(get_native(v1), get_native(v2)) self.assertEqual(get_native(value), native_value) # Create a variant from a type string. v3 = get_variant(expected_string, value) self.assertTrue(isinstance(v3, Variant)) self.assertTrue(v2.equal(v3)) def test_variant_invalid(self): """Test invalid variants.""" class UnknownType: pass with self.assertRaises(TypeError) as cm: get_variant(UnknownType, 1) self.assertEqual( "Invalid DBus type 'UnknownType'.", str(cm.exception) ) with self.assertRaises(TypeError) as cm: get_variant(Int, None) self.assertEqual( "Invalid DBus value 'None'.", str(cm.exception) ) with self.assertRaises(TypeError): get_variant(List[Int], True) def test_variant_basic(self): """Test variants with basic types.""" self._test_variant(Int, "i", 1) self._test_variant(Bool, "b", True) self._test_variant(Double, "d", 1.0) self._test_variant(Str, "s", "Hi!") self._test_variant(ObjPath, "o", "/org/something") def test_variant_int(self): """Test variants with integer types.""" self._test_variant(Int16, "n", 2) self._test_variant(UInt16, "q", 3) self._test_variant(Int32, "i", 4) self._test_variant(UInt32, "u", 5) self._test_variant(Int64, "x", 6) self._test_variant(UInt64, "t", 7) def test_variant_container(self): """Test variants with container types.""" self._test_variant(Tuple[Bool], "(b)", (False,)) self._test_variant(Tuple[Int, Str], "(is)", (0, "zero")) self._test_variant(List[Int], "ai", [1, 2, 3]) self._test_variant(List[Bool], "ab", [True, False, True]) self._test_variant(Dict[Str, Int], "a{si}", {"a": 1, "b": 2}) self._test_variant(Dict[Int, Bool], "a{ib}", {1: True, 2: False}) def test_variant_variant(self): """Test variants with variant types.""" variant = get_variant("i", 1) self._test_variant(Variant, "v", variant, 1) variant = get_variant("v", get_variant("i", 1)) self._test_variant(Variant, "v", variant, 1) variant = get_variant("(b)", (False,)) self._test_variant(Variant, "v", variant, (False,)) variant = get_variant("(v)", (get_variant("b", False),)) self._test_variant(Variant, "v", variant, (False,)) variant = get_variant("ai", [1, 2, 3]) self._test_variant(Variant, "v", variant, [1, 2, 3]) variant = get_variant("av", [ get_variant("i", 1), get_variant("i", 2), get_variant("i", 3) ]) self._test_variant(Variant, "v", variant, [1, 2, 3]) def test_variant_alias(self): """Test variants with type aliases.""" AliasType = List[Double] self._test_variant(Dict[Str, AliasType], "a{sad}", { "test": [1.1, 2.2] }) def _test_native(self, variants, values): """Test native values of variants.""" for variant, value in zip(variants, values): self.assertEqual(get_native(variant), value) self.assertEqual(get_native(tuple(variants)), tuple(values)) self.assertEqual(get_native(list(variants)), list(values)) self.assertEqual(get_native(dict(enumerate(variants))), dict(enumerate(values))) variant = get_variant( Tuple[Variant, Variant, Variant, Variant], tuple(variants) ) self.assertEqual(unwrap_variant(variant), tuple(variants)) variant = get_variant( List[Variant], list(variants) ) self.assertEqual(unwrap_variant(variant), list(variants)) variant = get_variant( Dict[Int, Variant], dict(enumerate(variants)) ) self.assertEqual(unwrap_variant(variant), dict(enumerate(variants))) def test_basic_native(self): """Test get_native with basic variants.""" self._test_native( [ get_variant(Double, 1.2), get_variant(List[Int], [0, -1]), get_variant(Tuple[Bool, Bool], (True, False)), get_variant(Dict[Str, Int], {"key": 0}), ], [ 1.2, [0, -1], (True, False), {"key": 0} ] ) def test_complex_native(self): """Test get_native with complex variants.""" self._test_native( [ get_variant(Variant, get_variant(Double, 1.2)), get_variant(List[Variant], [ get_variant(Int, 0), get_variant(Int, -1) ]), get_variant(Tuple[Variant, Bool], ( get_variant(Bool, True), False )), get_variant(Dict[Str, Variant], { "key": get_variant(Int, 0) }) ], [ 1.2, [0, -1], (True, False), {"key": 0} ] ) dasbus-1.7/tests/test_unix.py000066400000000000000000000374241433220137200164070ustar00rootroot00000000000000# # Copyright (C) 2022 Red Hat, Inc. All rights reserved. # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 # USA # import os import tempfile import unittest import unittest.mock from dasbus.connection import AddressedMessageBus from dasbus.typing import get_variant, UnixFD, Str, Variant, Tuple, \ List, Dict, Int, Double, Bool from dasbus.server.interface import dbus_interface, dbus_signal from dasbus.unix import GLibClientUnix, GLibServerUnix, acquire_fds, \ restore_fds from dasbus.xml import XMLGenerator from tests.lib_dbus import run_loop, DBusSpawnedTestCase from tests.test_dbus import DBusExampleTestCase, error_mapper import gi gi.require_version("Gio", "2.0") gi.require_version("GLib", "2.0") from gi.repository import Gio __all__ = [ "UnixFDSwapTests", "DBusSpawnedTestCase", ] def mocked(callback): """Wrap the local callback in a mock object.""" return unittest.mock.Mock(side_effect=callback) def read_string(fd): """Read a value from the given file descriptor.""" with open(os.dup(fd), "rb", closefd=True) as i: value = i.read().decode("utf-8") return value def write_string(value): """Write a value to the given file descriptor.""" with tempfile.TemporaryFile(mode="wb", buffering=0) as o: o.write(value.encode("utf-8")) o.seek(0) out = os.dup(o.fileno()) return UnixFD(out) @dbus_interface("my.testing.UnixExample") class UnixExampleInterface(object): """A DBus interface with UnixFD types.""" def __init__(self): self._pipes = [] @property def Pipes(self) -> List[UnixFD]: return self._pipes @Pipes.setter def Pipes(self, pipes: List[UnixFD]): self._pipes = pipes def Hello(self, name_fd: UnixFD) -> Str: name = read_string(name_fd) return "Hello, {0}!".format(name) def Goodbye(self, name: Str) -> UnixFD: return write_string("Goodbye, {0}!".format(name)) @dbus_signal def Signal(self, name: Str, name_fd: UnixFD): pass def Trigger(self, name: Str, name_fd: UnixFD): self.Signal(name, name_fd) class UnixMessageBus(AddressedMessageBus): """A message bus connection with the UnixFD support.""" def publish_object(self, *args, **kwargs): return super().publish_object(*args, **kwargs, server=GLibServerUnix) def get_proxy(self, *args, **kwargs): return super().get_proxy(*args, **kwargs, client=GLibClientUnix) class UnixFDSwapTests(unittest.TestCase): """Test swapping of Unix file descriptors.""" def setUp(self): """Set up the test.""" self._r, self._w = os.pipe() def tearDown(self): """Tear down the test.""" os.close(self._r) os.close(self._w) def _swap_fds(self, in_variant, out_variant, fds=None): """Swap Unix file descriptors in the input.""" variant, fd_list = self._acquire_fds(in_variant, out_variant, fds) self._restore_fds(variant, fd_list, expected_variant=in_variant) def _acquire_fds(self, variant, expected_variant, expected_fds): """Acquire Unix file descriptors in the variant.""" variant, fd_list = acquire_fds(variant) # Check the list of acquired fds. if expected_fds is None: self.assertIsNone(fd_list) else: self.assertIsInstance(fd_list, Gio.UnixFDList) self.assertEqual(fd_list.peek_fds(), expected_fds) # Check the variant without fds. if expected_variant is None: self.assertIsNone(variant) else: self.assertIsInstance(variant, Variant) self.assertEqual(variant.unpack(), expected_variant.unpack()) self.assertTrue(variant.equal(expected_variant)) return variant, fd_list def _restore_fds(self, variant, fd_list, expected_variant): """Restore Unix file descriptors in the variant.""" variant = restore_fds(variant, fd_list) # Check the variant with fds. if expected_variant is None: self.assertIsNone(variant) else: self.assertIsInstance(variant, Variant) self.assertEqual(variant.unpack(), expected_variant.unpack()) self.assertTrue(variant.equal(expected_variant)) def test_empty_fd_list(self): """Restore a value if the fd list is empty.""" self._restore_fds( variant=get_variant(Int, 0), fd_list=Gio.UnixFDList(), expected_variant=get_variant(Int, 0) ) def test_invalid_index(self): """Restore a value with an invalid index.""" self._restore_fds( variant=get_variant(UnixFD, 2), fd_list=Gio.UnixFDList.new_from_array([self._r, self._w]), expected_variant=get_variant(UnixFD, -1) ) def test_values_without_fds(self): """Swap values without fds.""" self._swap_fds( in_variant=None, out_variant=None, ) self._swap_fds( in_variant=get_variant(Int, 0), out_variant=get_variant(Int, 0), ) self._swap_fds( in_variant=get_variant(Str, "Hi!"), out_variant=get_variant(Str, "Hi!"), ) self._swap_fds( in_variant=get_variant(Tuple[Double], (1.0, )), out_variant=get_variant(Tuple[Double], (1.0, )), ) self._swap_fds( in_variant=get_variant(List[Bool], [False]), out_variant=get_variant(List[Bool], [False]), ) def test_value_with_fds(self): """Swap a basic value with fds.""" self._swap_fds( in_variant=get_variant(UnixFD, self._r), out_variant=get_variant(UnixFD, 0), fds=[self._r], ) def test_variant_with_fds(self): """Swap a variant with fds.""" self._swap_fds( in_variant=get_variant(Variant, get_variant( UnixFD, self._r )), out_variant=get_variant(Variant, get_variant( UnixFD, 0 )), fds=[self._r], ) def test_tuple_with_fds(self): """Swap a tuple with fds.""" self._swap_fds( in_variant=get_variant(Tuple[UnixFD, UnixFD], ( self._r, self._w )), out_variant=get_variant(Tuple[UnixFD, UnixFD], ( 0, 1 )), fds=[self._r, self._w], ) def test_variant_tuple_with_fds(self): """Swap a tuple of variants with fds.""" self._swap_fds( in_variant=get_variant(Tuple[Variant, Variant], ( get_variant(UnixFD, self._r), get_variant(UnixFD, self._w), )), out_variant=get_variant(Tuple[Variant, Variant], ( get_variant(UnixFD, 0), get_variant(UnixFD, 1), )), fds=[self._r, self._w], ) def test_list_with_fds(self): """Swap a list with fds.""" self._swap_fds( in_variant=get_variant(List[UnixFD], [ self._r, self._w ]), out_variant=get_variant(List[UnixFD], [ 0, 1 ]), fds=[self._r, self._w], ) def test_variant_list_with_fds(self): """Swap a list of variants with fds.""" self._swap_fds( in_variant=get_variant(List[Variant], [ get_variant(UnixFD, self._r), get_variant(UnixFD, self._w) ]), out_variant=get_variant(List[Variant], [ get_variant(UnixFD, 0), get_variant(UnixFD, 1) ]), fds=[self._r, self._w], ) def test_dictionary_with_fds_values(self): """Swap a dictionary with fds values.""" self._swap_fds( in_variant=get_variant(Dict[Str, UnixFD], { "r": self._r, "w": self._w }), out_variant=get_variant(Dict[Str, UnixFD], { "r": 0, "w": 1 }), fds=[self._r, self._w] ) def test_dictionary_with_fds_keys(self): """Swap a dictionary with fds keys.""" self._swap_fds( in_variant=get_variant(Dict[UnixFD, Str], { self._r: "r", self._w: "w" }), out_variant=get_variant(Dict[UnixFD, Str], { 0: "r", 1: "w" }), fds=[self._r, self._w] ) def test_variant_dictionary_with_fds(self): """Swap a dictionary of variants with fds.""" self._swap_fds( in_variant=get_variant(Dict[Str, Variant], { "r": get_variant(UnixFD, self._r), "w": get_variant(UnixFD, self._w) }), out_variant=get_variant(Dict[Str, Variant], { "r": get_variant(UnixFD, 0), "w": get_variant(UnixFD, 1) }), fds=[self._r, self._w] ) def test_nested_variants_with_fds(self): """Swap nested variants with fds.""" self._swap_fds( in_variant=get_variant(Variant, get_variant( Variant, get_variant(UnixFD, self._r)), ), out_variant=get_variant(Variant, get_variant( Variant, get_variant(UnixFD, 0)), ), fds=[self._r], ) def test_nested_containers_with_fds(self): """Swap nested containers with fds.""" self._swap_fds( in_variant=get_variant( Tuple[List[UnixFD], Dict[Str, Variant]], ([self._r], {"w": get_variant(UnixFD, self._w)}) ), out_variant=get_variant( Tuple[List[UnixFD], Dict[Str, Variant]], ([0], {"w": get_variant(UnixFD, 1)}) ), fds=[self._r, self._w], ) class DBusUnixCompatibilityTestCase(DBusExampleTestCase): """Test the Unix support compatibility with a real DBus connection.""" def _get_message_bus(self, bus_address): """Get a message bus.""" return UnixMessageBus(bus_address, error_mapper) class DBusUnixExampleTestCase(DBusSpawnedTestCase): """Test Unix support with a real DBus connection.""" def _get_service(self): """Get the example service.""" return UnixExampleInterface() @classmethod def _get_message_bus(cls, bus_address): """Get a message bus.""" return UnixMessageBus(bus_address) @classmethod def _get_proxy(cls, bus_address, **proxy_args): """Get a proxy of the example service.""" message_bus = cls._get_message_bus(bus_address) return cls._get_service_proxy(message_bus, **proxy_args) def test_xml_specification(self): """Test the generated specification.""" expected_xml = ''' ''' generated_xml = self.service.__dbus_xml__ self.assertEqual( XMLGenerator.prettify_xml(expected_xml), XMLGenerator.prettify_xml(generated_xml) ) def test_sync_calls(self): """Test DBus sync calls with fds.""" self._add_client(self._call_hello_sync) self._add_client(self._call_goodbye_sync) self._run_test() @classmethod def _call_hello_sync(cls, bus_address): """Say sync hello to Foo.""" proxy = cls._get_proxy(bus_address) fd = write_string("Foo") greeting = proxy.Hello(fd) assert greeting == "Hello, Foo!", greeting @classmethod def _call_goodbye_sync(cls, bus_address): """Say sync goodbye to Bar.""" proxy = cls._get_proxy(bus_address) fd = proxy.Goodbye("Bar") greeting = read_string(fd) assert greeting == "Goodbye, Bar!", greeting def test_async_calls(self): """Test DBus async calls with fds.""" self._add_client(self._call_hello_async) self._add_client(self._call_goodbye_async) self._run_test() @classmethod def _call_hello_async(cls, bus_address): """Say async hello to Foo.""" proxy = cls._get_proxy(bus_address) @mocked def callback(call): greeting = call() assert greeting == "Hello, Foo!", greeting fd = write_string("Foo") proxy.Hello(fd, callback=callback) run_loop() callback.assert_called_once() @classmethod def _call_goodbye_async(cls, bus_address): """Say async goodbye to Bar.""" proxy = cls._get_proxy(bus_address) @mocked def callback(call): greeting = read_string(call()) assert greeting == "Goodbye, Bar!", greeting proxy.Goodbye("Bar", callback=callback) run_loop() callback.assert_called_once() def test_properties(self): """Test DBus properties with fds.""" self._add_client(self._set_pipes) self._add_client(self._get_pipes) self._run_test() @classmethod def _set_pipes(cls, bus_address): proxy = cls._get_proxy(bus_address) pipes = list(map(write_string, ["1", "2", "3"])) proxy.Pipes = pipes @classmethod def _get_pipes(cls, bus_address): proxy = cls._get_proxy(bus_address) pipes = [] while not pipes: pipes = proxy.Pipes values = list(map(read_string, pipes)) assert values == ["1", "2", "3"] def test_signals(self): """Test DBus signals with fds.""" event = self.context.Event() self._add_client(self._trigger_signal, event) self._add_client(self._watch_signal, event) self._run_test() @classmethod def _trigger_signal(cls, bus_address, event): event.wait() proxy = cls._get_proxy(bus_address) proxy.Trigger("Foo", write_string("Foo")) @classmethod def _watch_signal(cls, bus_address, event): proxy = cls._get_proxy(bus_address) @mocked def callback(name, name_fd): # GLib doesn't support fds in signals, so we # are not able to restore fds in this case. # Because of that, name_fd is just an index. assert name == "Foo" assert name_fd == 0 proxy.Signal.connect(callback) event.set() run_loop() callback.assert_called_once() dasbus-1.7/tests/test_xml.py000066400000000000000000000151571433220137200162230ustar00rootroot00000000000000# # Copyright (C) 2019 Red Hat, Inc. All rights reserved. # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 # USA # import unittest from dasbus.xml import XMLParser, XMLGenerator class XMLParserTestCase(unittest.TestCase): def test_is_member(self): """Test if the element is a member of an interface.""" element = XMLParser.xml_to_element('') self.assertEqual(XMLParser.is_member(element), True) element = XMLParser.xml_to_element('') self.assertEqual(XMLParser.is_member(element), True) element = XMLParser.xml_to_element( '' ) self.assertEqual(XMLParser.is_member(element), True) def test_is_interface(self): """Test if the element is an interface.""" element = XMLParser.xml_to_element( '' ) self.assertEqual(XMLParser.is_interface(element), True) def test_is_signal(self): """Test if the element is a signal.""" element = XMLParser.xml_to_element('') self.assertEqual(XMLParser.is_signal(element), True) def test_is_method(self): """Test if the element is a method.""" element = XMLParser.xml_to_element('') self.assertEqual(XMLParser.is_method(element), True) def test_is_property(self): """Test if the element is a property.""" element = XMLParser.xml_to_element( '' ) self.assertEqual(XMLParser.is_property(element), True) def test_is_parameter(self): """Test if the element is a parameter.""" element = XMLParser.xml_to_element( '' ) self.assertEqual(XMLParser.is_parameter(element), True) def test_has_name(self): """Test if the element has the specified name.""" element = XMLParser.xml_to_element('') self.assertEqual(XMLParser.has_name(element, "MethodName"), True) self.assertEqual(XMLParser.has_name(element, "AnotherName"), False) def test_get_name(self): """Get the name attribute.""" element = XMLParser.xml_to_element('') self.assertEqual(XMLParser.get_name(element), "MethodName") def test_get_type(self): """Get the type attribute.""" element = XMLParser.xml_to_element( '' ) self.assertEqual( XMLParser.get_type(element), "ParameterType" ) def test_get_access(self): """Get the access attribute.""" element = XMLParser.xml_to_element( '' ) self.assertEqual( XMLParser.get_access(element), "PropertyAccess" ) def test_get_direction(self): """Get the direction attribute.""" element = XMLParser.xml_to_element( '' ) self.assertEqual( XMLParser.get_direction(element), "ParameterDirection" ) def test_get_interfaces_from_node(self): """Get interfaces from the node.""" element = XMLParser.xml_to_element(''' ''') interfaces = XMLParser.get_interfaces_from_node(element) self.assertEqual(interfaces.keys(), {"A", "B", "C"}) class XMLGeneratorTestCase(unittest.TestCase): def _compare(self, element, xml): self.assertEqual( XMLGenerator.prettify_xml( XMLGenerator.element_to_xml(element) ), XMLGenerator.prettify_xml(xml) ) def test_node(self): """Test the node element.""" self._compare(XMLGenerator.create_node(), '') def test_interface(self): """Test the interface element.""" self._compare( XMLGenerator.create_interface("InterfaceName"), '' ) def test_parameter(self): """Test the parameter element.""" self._compare( XMLGenerator.create_parameter( "ParameterName", "ParameterType", "ParameterDirection" ), '' ) def test_property(self): """Test the property element.""" self._compare( XMLGenerator.create_property( "PropertyName", "PropertyType", "PropertyAccess" ), '' ) def test_method(self): """Test the method element.""" element = XMLGenerator.create_method("MethodName") xml = '' self._compare(element, xml) def test_signal(self): """Test the signal element.""" element = XMLGenerator.create_signal("SignalName") xml = '' self._compare(element, xml)