diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..3cb7e1d --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,2 @@ +patreon: igo95862 +liberapay: igo95862 diff --git a/.github/workflows/ubuntu_pypi_test.yml b/.github/workflows/ubuntu_pypi_test.yml index 07c0724..495bc7f 100644 --- a/.github/workflows/ubuntu_pypi_test.yml +++ b/.github/workflows/ubuntu_pypi_test.yml @@ -1,26 +1,42 @@ --- -name: Install package from PyPI and run unit tests on Ubuntu 20.04 +name: Install package from PyPI and run unit tests on: workflow_dispatch: + inputs: + pypi_version: + description: "Version specifier to install from PyPI" + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: false jobs: run: - name: Install from PyPI and run unit tests - runs-on: ubuntu-20.04 + name: Install PyPI binary package and run unit tests + strategy: + matrix: + ubuntu_version: + - "ubuntu-22.04" + - "ubuntu-24.04" + - "ubuntu-22.04-arm" + - "ubuntu-24.04-arm" + runs-on: ${{ matrix.ubuntu_version }} steps: - name: Checkout - uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 - name: Install dependencies run: | - sudo apt update - sudo apt install python3-setuptools \ - systemd dbus python3 python3-pip python3-jinja2 + sudo apt-get update + sudo apt-get install dbus python3 python3-pip python3-venv - name: Install package run: | - sudo pip3 install sdbus>=0.8rc2 + python3 -m venv venv + ./venv/bin/pip3 install --only-binary ':all:' "sdbus ${SDBUS_VERSION}" + env: + SDBUS_VERSION: ${{ inputs.pypi_version }} - name: List package run: | - pip3 list | grep sdbus + ./venv/bin/pip3 list | grep sdbus - name: Run unit tests run: | - python3 -m unittest + ./venv/bin/python3 -m unittest diff --git a/.github/workflows/ubuntu_test.yml b/.github/workflows/ubuntu_test.yml index 3b496de..6a028ea 100644 --- a/.github/workflows/ubuntu_test.yml +++ b/.github/workflows/ubuntu_test.yml @@ -2,17 +2,18 @@ name: CI on: push: - branches: [master] pull_request: workflow_dispatch: + schedule: + - cron: '0 0 * * 5' jobs: unlimited: name: Run build and unit tests. (unlimited API) - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - name: Checkout - uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 - name: Install dependencies run: | sudo apt update @@ -26,10 +27,10 @@ jobs: PYTHONPATH=./build-lib python3 -m unittest --verbose limited: name: Run build and unit tests. (limited API) - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - name: Checkout - uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 - name: Install dependencies run: | sudo apt update @@ -48,29 +49,32 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 - name: Install dependencies run: | sudo apt update - sudo apt install python3 python3-pip meson - sudo pip3 install --upgrade mypy isort flake8 pyflakes pycodestyle \ - jinja2 Sphinx types-setuptools + sudo apt install python3 python3-pip ninja-build + python -m venv --system-site-packages venv + ./venv/bin/pip install --upgrade \ + mypy isort flake8 pyflakes pycodestyle \ + jinja2 'Sphinx<8.0' types-setuptools meson - name: Run linters run: | + export PATH="$(readlink -f ./venv/bin):${PATH}" meson setup build - ninja -C build lint-python + meson compile -C build lint-python alpine: name: Alpine Linux test runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 - name: Build Alpine container run: | podman build --tag alpine-ci -f ./test/containers/Containerfile-alpine . - name: Test unlimited API run: | - podman run --rm -it alpine-ci + podman run --rm alpine-ci - name: Test limited API run: | - podman run --env PYTHON_SDBUS_USE_LIMITED_API=1 --rm -it alpine-ci + podman run --env PYTHON_SDBUS_USE_LIMITED_API=1 --rm alpine-ci diff --git a/.python-version b/.python-version deleted file mode 100644 index 2e40913..0000000 --- a/.python-version +++ /dev/null @@ -1,2 +0,0 @@ -3.8.7 -system diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 2112b9c..2ad865e 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -2,5 +2,14 @@ version: 2 +build: + os: "ubuntu-22.04" + tools: + python: "3.9" + +sphinx: + configuration: "docs/conf.py" + python: - version: 3.8 + install: + - requirements: docs/requirements.txt diff --git a/CHANGELOG.md b/CHANGELOG.md index 675fc27..5c9a348 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,207 @@ +## 0.14.2 + +### Fixes + +* Fix segmentation fault if export handle outlives the exported object. (reported by @arkq) +* Fix some tests failing on slow systems. + +## 0.14.1 + +### Features + +* Added `sdbus.utils.inspect.inspect_dbus_bus` function. Returns a bus object + used by a proxy or exported local object. + +### Fixes + +* Fix object manager's `InterfacesRemoved` signal being emitted without having + interface names. (reported and fixed by @arkq) +* Fixed interface ordering for signals and methods that return interface information + like `InterfacesAdded` or `GetManagedObjects`. (reported and fixed by @arkq) +* Fixed exported methods callbacks sometimes getting garbage collected before + reply could be sent. (reported by @arkq) +* Fixed several documentation URLs linking outdated repository. (reported and fixed by @rmelotte) + +## 0.14.0 + +### Minimum requirements raised + +* Python 3.9 or higher. + +For binary PyPI wheel: + +* glibc 2.28 or higher. (Debian 10+, Ubuntu 18.10+, CentOS/RHEL 8+) +* pip 20.3 or higher. +* Added 32 bit ARM (`armv7l`) architecture wheel. + +### Default bus changes + +Previously the default bus always used the context-local variables +to store the reference to the current default bus. As it turned out +the context tends to be changed a lot which resulted in new buses being +opened multiple times. (reported by @wes8ty) + +To avoid this the default bus was changed to be thread-local. +`set_default_bus` will now set the thread-local default bus. +A new function `set_context_default_bus` was added to set the context-local +bus. The `get_default_bus` will return the context-local bus if set or +thread-local otherwise. If no default bus has been set a new thread-local +bus will be initialized and set. + +### Code generator + +* Code generator will now add manual D-Bus member name override where + automatic snake_case to CamelCase does not result in the original member name. + This applies to when member renaming options were used. (reported by @nicomuns) +* Generated code will now use Python 3.9 built-in collections type hints. + (`typing.List[str]` -> `list[str]`) +* Fixed blocking generated code adding unexpected `result_args_names` keyword. + (reported by @christophehenry) + +### Features + +* All `sdbus.utils.parse` functions can now accept the blocking interfaces. + (requested by @christophehenry) +* Added boolean `use_interface_subsets` option to `sdbus.utils.parse` functions. + When enabled the subset of interfaces will be considered a valid match. + (requested by @christophehenry) + +### Fixes + +* Fixed exceptions mapped by `map_exception_to_dbus_error` not being translated + from Python to D-Bus errors. This means the Python built-in exceptions will + now be properly returned as D-Bus errors when raised in exported object callback. + The built-in exceptions translating as added back in version 0.10.0 but probably + never worked correctly. (reported by @arkq) +* Fixed not being able to export interfaces with no implemented D-Bus members. + This also means `export_to_dbus` will only access D-Bus related attributes + avoiding triggering unrelated `@property` methods. +* Renamed certain internal classes from `Binded` to `Bound` and + from `DbusSomething` to `DbusMember`. (reported by @souliane, + implemented by @dragomirecky) + +## 0.13.0 + +### Code generator improvements + +* Added interface and member renaming CLI options. `--select-interface`, `--select-method`, + `--select-property` and `--select-signal` will select a particular interface or member and + `--set-name` will set the selected interface or member Python name. +* Fix generated D-Bus properties not using emits changed flag by default. +* Fix generated D-Bus methods not using unprivileged flag by default. (reported by @damienklotz77) +* Generated methods and signals will now have result argument names set which will be shown + in the introspection. (requested by @colazzo) + +### New `sdbus.utils.inspect` submodule + +Contains inspection utilities. + +Current only provides the `inspect_dbus_path` function which will return +the D-Bus path of either proxy or exported object. (requested by ) + +### New `sdbus.utils.parse` submodule + +The existing `parse_properties_changed`, `parse_interfaces_added`, `parse_interfaces_removed` and +`parse_get_managed_objects` have been moved from from `sdbus.utils` to `sdbus.utils.parse`. + +For backwards compatibility `sdbus.utils` re-exports those functions but no new exports will be +added to it. + +### Fixes + +* Fix bus timeouts not being processed on time. (requested by @ofacklam) + +## 0.12.0 + +No changes since 0.12.RC1. + +## 0.12.RC1 + +This version significantly reworked the internal undocumented classes +and functions. If you used the undocumented API you would probably need +to adjust your code. Type checker like `mypy` can be very useful for this. + +### Features: + +* `@setter_private` can now be used in overrides. +* Added `assertDbusSignalEmits` method to `IsolatedDbusTestCase`. + Can be used to assert that a D-Bus signal was emitted + inside the `async with` block. +* Added `sdbus.utils.parse_get_managed_objects` function. Can be + used to parse the ObjectManager's `get_managed_objects` method + data to classes and Python attribute names. +* Added a handle that is returned by `export_to_dbus` and `export_with_manager` + methods. This handle can be used to explicitly control when object is accessible + from D-Bus. (requested by @dragomirecky) + +### Fixes: + +* Fixed async D-Bus properties not having a proper generic typing. (reported by @ValdezFOmar) +* Fixed build not working when systemd has a minor version suffix. +* Fixed being unable to name arguments in D-Bus introspection when + method has no return arguments. (reported by @colazzo) +* Fixed serving D-Bus methods that return a single struct. (reported by @colazzo) +* Fixed sending extremely large D-Bus messages getting stuck. (reported by @colazzo) + +## 0.11.1 + +### Features: + +* Improved interface generator handling of multiple uppercase letters + sequences. For example, `ACTIVATE_CONNECTION` would before be converted + to `a_c_t_i_v_a_t_e__c_o_n_n_e_c_t_i_o_n` and after to `activate_connection`. + (reported by @bhattarabi) +* Improved python formatting generated by interface code generator. +* Added option `--block` to generate blocking interface code. + (requested by @zhanglongqi and @MathisMARION) + +### Fixes: + +* Fixed docstrings still being present even if python was configured with + `--without-doc-strings`. +* Fixed interface generator crashing when a rare write-only property is + encountered. (reported by @gotthardp) +* Fixed async interfaces iterating over all members during initialization. + (reported by @gotthardp) +* Fixed `TypeError: Dbus type '\x00' is unknown` being raised when trying to read + from a message more than one time. (reported by @IB1387 and @asmello) +* Fixed missing class body when generating code for interface without members. + +## 0.11.0 + +### Features: + +* Added support for `None` signals without data. +* Added boolean flags for the name request functions + which can be used to specify replacements or queueing. +* Added `sdbus.utils.parse_properties_changed` helper function. + Parses signal data to python member names and values. +* Added `sdbus.utils.parse_interfaces_added` helper function. + Parses signal data to path, python class and python member names + and values. +* Added `sdbus.utils.parse_interfaces_removed` helper function. + Parses signal data to path and python class. +* Added `setter_private` decorator to async properties. Private + setter can only be called locally but to D-Bus property will + appear as read only. +* Added new exceptions for when D-Bus name requests fail. + * `SdBusRequestNameExistsError`: Someone already owns name. + * `SdBusRequestNameAlreadyOwnerError`: Caller already owns name. + * `SdBusRequestNameInQueueError`: Name request queued up. + +### Deprecations: + +* Moved all exceptions to `sdbus.exceptions` module. + For backwards compatibility old exceptions will be + available from the root module until the version `1.0.0`. + +### Fixes: + +* Fixed autodoc adding `dbus_method` to dbus methods names +* Fix async D-Bus name requests not raising appropriate exceptions. +* Fixed `request_default_bus_name` being an async function. + For backwards compatibility it returns an awaitable that raises a warning. + ## 0.10.2 ### Features: diff --git a/DEPRECATIONS.md b/DEPRECATIONS.md index 1f7e239..0da80d0 100644 --- a/DEPRECATIONS.md +++ b/DEPRECATIONS.md @@ -1,5 +1,23 @@ # Deprecation information +## Awaiting on `request_default_bus_name` + +By mistake `request_default_bus_name` was made in to async function +even though it was never documented to be one. It is now a blocking +function but returns an awaitable for backwards compatibility. + +* **Since**: 0.11.0 +* **Warning**: 0.11.0 +* **Removed**: 0.14.0 + +## Importing exceptions from `sdbus` module + +All exceptions have been moved to `sdbus.exceptions` to clean up imports. + +* **Since**: 0.11.0 +* **Warning**: Not possible? +* **Removed**: 1.0.0 + ## `_connect` and `new_connect` of the `DbusInterfaceCommonAsync` class Replaced with equivalent `_proxify` and `new_proxy`. diff --git a/README.md b/README.md index aa22e25..da37713 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,12 @@ -[![Total alerts](https://img.shields.io/lgtm/alerts/g/igo95862/python-sdbus.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/igo95862/python-sdbus/alerts/) -[![Language grade: Python](https://img.shields.io/lgtm/grade/python/g/igo95862/python-sdbus.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/igo95862/python-sdbus/context:python) [![Documentation Status](https://readthedocs.org/projects/python-sdbus/badge/?version=latest)](https://python-sdbus.readthedocs.io/en/latest/?badge=latest) +[![PyPI - Version](https://img.shields.io/pypi/v/sdbus)](https://pypi.org/project/sdbus/) # Modern Python library for D-Bus + + Packaging status + + Features: * Asyncio and blocking calls. @@ -11,7 +14,7 @@ Features: * No Python 2 legacy. * Based on fast sd-bus from systemd. (also supports elogind) * Unified client/server interface classes. Write interface once! -* Dbus methods can have keyword and default arguments. +* D-Bus methods can have keyword and default arguments. See the [documentation](https://python-sdbus.readthedocs.io/en/latest/index.html) @@ -26,17 +29,34 @@ for tutorial and API reference. More incoming. (systemd, Bluez, screen saver... ) +### Community interfaces + +* [systemd](https://github.com/bernhardkaindl/python-sdbus-systemd) (by [@bernhardkaindl](https://github.com/bernhardkaindl)) +* [modemmanager](https://github.com/zhanglongqi/python-sdbus-modemmanager) (by [@zhanglongqi](https://github.com/zhanglongqi)) + +## Stability + +Python-sdbus is under development and its API is not stable. Generally +anything documented in the official documentation is considered +stable but might be deprecated. Using deprecated feature will +raise a warning and the feature will be eventually removed. + +See the [deprecations list](DEPRECATIONS.md). + +If there is a feature that is not documented but you would like to use +please open a new issue. + ## Requirements ### Binary package from PyPI -* Python 3.8 or higher. (3.7 might work but is not supported) -* `x86_64` or `aarch64` architecture. -* glibc 2.17 or higher. (released in 2014) -* pip 19.3 or higher. +* Python 3.9 or higher. +* `x86_64`, `aarch64` or `armv7l` architecture. +* glibc 2.28 or higher. (Debian 10+, Ubuntu 18.10+, CentOS/RHEL 8+) +* pip 20.3 or higher. -Starting with version `0.8rc2` the libsystemd is statically -linked and is not required. +`libsystemd` is statically linked and is not required to be installed +on the system. Pass `--only-binary ':all:'` to pip to ensure that it installs binary package. @@ -47,7 +67,7 @@ platforms. ### Source package or compiling from source -* Python 3.8 or higher. +* Python 3.9 or higher. * Python headers. (`python3-dev` package on ubuntu) * GCC. * libsystemd or libelogind @@ -141,7 +161,7 @@ async def startup() -> None: # Acquire a known name on the bus # Clients will use that name to address this server await request_default_bus_name_async('org.example.test') - # Export the object to dbus + # Export the object to D-Bus export_object.export_to_dbus('/') @@ -180,10 +200,16 @@ async def get_hello_world() -> None: loop = new_event_loop() -# Always binds your tasks to a variable +# Always bind your tasks to a variable task_upper = loop.create_task(call_upper()) task_clock = loop.create_task(print_clock()) task_hello_world = loop.create_task(get_hello_world()) loop.run_forever() ``` + +## License + +Python-sdbus is licensed under [LGPL-2.1-or-later](https://spdx.org/licenses/LGPL-2.1-or-later.html). + +The LGPL license is an extension of GPL license therefore both licenses' texts are required. diff --git a/docs/api_index.rst b/docs/api_index.rst index df92255..bbb4c17 100644 --- a/docs/api_index.rst +++ b/docs/api_index.rst @@ -65,70 +65,82 @@ Blocking: Exceptions: ++++++++++++++++++++++++++ -:py:exc:`DbusAccessDeniedError` +:py:exc:`exceptions.DbusAccessDeniedError` -:py:exc:`DbusAddressInUseError` +:py:exc:`exceptions.DbusAccessDeniedError` -:py:exc:`DbusAuthFailedError` +:py:exc:`exceptions.DbusAddressInUseError` -:py:exc:`DbusBadAddressError` +:py:exc:`exceptions.DbusAuthFailedError` -:py:exc:`DbusDisconnectedError` +:py:exc:`exceptions.DbusBadAddressError` -:py:exc:`DbusFailedError` +:py:exc:`exceptions.DbusDisconnectedError` -:py:exc:`DbusFileExistsError` +:py:exc:`exceptions.DbusFailedError` -:py:exc:`DbusFileNotFoundError` +:py:exc:`exceptions.DbusFileExistsError` -:py:exc:`DbusInconsistentMessageError` +:py:exc:`exceptions.DbusFileNotFoundError` -:py:exc:`DbusInteractiveAuthorizationRequiredError` +:py:exc:`exceptions.DbusInconsistentMessageError` -:py:exc:`DbusInvalidArgsError` +:py:exc:`exceptions.DbusInteractiveAuthorizationRequiredError` -:py:exc:`DbusInvalidFileContentError` +:py:exc:`exceptions.DbusInvalidArgsError` -:py:exc:`DbusInvalidSignatureError` +:py:exc:`exceptions.DbusInvalidFileContentError` -:py:exc:`DbusIOError` +:py:exc:`exceptions.DbusInvalidSignatureError` -:py:exc:`DbusLimitsExceededError` +:py:exc:`exceptions.DbusIOError` -:py:exc:`DbusMatchRuleInvalidError` +:py:exc:`exceptions.DbusLimitsExceededError` -:py:exc:`DbusMatchRuleNotFound` +:py:exc:`exceptions.DbusMatchRuleInvalidError` -:py:exc:`DbusNameHasNoOwnerError` +:py:exc:`exceptions.DbusMatchRuleNotFound` -:py:exc:`DbusNoMemoryError` +:py:exc:`exceptions.DbusNameHasNoOwnerError` -:py:exc:`DbusNoNetworkError` +:py:exc:`exceptions.DbusNoMemoryError` -:py:exc:`DbusNoReplyError` +:py:exc:`exceptions.DbusNoNetworkError` -:py:exc:`DbusNoServerError` +:py:exc:`exceptions.DbusNoReplyError` -:py:exc:`DbusNotSupportedError` +:py:exc:`exceptions.DbusNoServerError` -:py:exc:`DbusPropertyReadOnlyError` +:py:exc:`exceptions.DbusNotSupportedError` -:py:exc:`DbusServiceUnknownError` +:py:exc:`exceptions.DbusPropertyReadOnlyError` -:py:exc:`DbusTimeoutError` +:py:exc:`exceptions.DbusServiceUnknownError` -:py:exc:`DbusUnixProcessIdUnknownError` +:py:exc:`exceptions.DbusTimeoutError` -:py:exc:`DbusUnknownInterfaceError` +:py:exc:`exceptions.DbusUnixProcessIdUnknownError` -:py:exc:`DbusUnknownMethodError` +:py:exc:`exceptions.DbusUnknownInterfaceError` -:py:exc:`DbusUnknownObjectError` +:py:exc:`exceptions.DbusUnknownMethodError` -:py:exc:`DbusUnknownPropertyError` +:py:exc:`exceptions.DbusUnknownObjectError` -:py:exc:`SdBusBaseError` +:py:exc:`exceptions.DbusUnknownPropertyError` -:py:exc:`SdBusLibraryError` +:py:exc:`exceptions.SdBusBaseError` -:py:exc:`SdBusUnmappedMessageError` \ No newline at end of file +:py:exc:`exceptions.SdBusLibraryError` + +:py:exc:`exceptions.SdBusUnmappedMessageError` + +:py:func:`exceptions.map_exception_to_dbus_error` + +:py:exc:`exceptions.SdBusRequestNameError` + +:py:exc:`exceptions.SdBusRequestNameInQueueError` + +:py:exc:`exceptions.SdBusRequestNameExistsError` + +:py:exc:`exceptions.SdBusRequestNameAlreadyOwnerError` diff --git a/docs/asyncio_api.rst b/docs/asyncio_api.rst index ab14b33..3411f2f 100644 --- a/docs/asyncio_api.rst +++ b/docs/asyncio_api.rst @@ -8,8 +8,8 @@ Classes .. py:class:: DbusInterfaceCommonAsync(interface_name) - Dbus async interface class. - Dbus methods and properties should be defined using + D-Bus async interface class. + D-Bus methods and properties should be defined using :py:func:`dbus_property_async`, :py:func:`dbus_signal_async`, and :py:func:`dbus_method_async` decorators. @@ -17,19 +17,19 @@ Classes Don't forget to call ``super().__init__()`` in derived classes init calls as it sets up important attributes. - :param str interface_name: Sets the dbus interface + :param str interface_name: Sets the D-Bus interface name that will be used for all properties, methods and signals defined in the body of the class. :param bool serving_enabled: If set to :py:obj:`True` - the interface will not be served on dbus. Mostly used + the interface will not be served on D-Bus. Mostly used for interfaces that sd-bus already provides such as ``org.freedesktop.DBus.Peer``. .. py:method:: dbus_ping() :async: - Pings the remote service using dbus. + Pings the remote service using D-Bus. Useful to test if connection or remote service is alive. @@ -47,7 +47,7 @@ Classes .. py:method:: dbus_introspect() :async: - Get dbus introspection XML. + Get D-Bus introspection XML. It is users responsibility to parse that data. @@ -68,71 +68,109 @@ Classes either raise an ``"error"`` (default), ``"ignore"`` the property or ``"reuse"`` the D-Bus name for the member. :return: dictionary of properties - :rtype: Dict[str, Any] + :rtype: dict[str, Any] .. py:attribute:: properties_changed - :type: Tuple[str, Dict[str, Tuple[str, Any]], List[str]] + :type: tuple[str, dict[str, tuple[str, Any]], list[str]] Signal when one of the objects properties changes. + :py:func:`sdbus.utils.parse.parse_properties_changed` can be used to transform + this signal data in to an easier to work with dictionary. + Signal data is: Interface name : str Name of the interface where property changed - Changed properties : Dict[str, Tuple[str, Any]] + Changed properties : dict[str, tuple[str, Any]] Dictionary there keys are names of properties changed and values are variants of new value. - Invalidated properties : List[str] + Invalidated properties : list[str] List of property names changed but no new value had been provided .. py:method:: _proxify(bus, service_name, object_path) - Begin proxying to a remote dbus object. + Begin proxying to a remote D-Bus object. :param str service_name: - Remote object dbus connection name. + Remote object D-Bus connection name. For example, systemd uses ``org.freedesktop.systemd1`` :param str object_path: - Remote object dbus path. + Remote object D-Bus path. Should be a forward slash separated path. Starting object is usually ``/``. Example: ``/org/freedesktop/systemd/unit/dbus_2eservice`` :param SdBus bus: - Optional dbus connection object. - If not passed the default dbus will be used. + Optional D-Bus connection object. + If not passed the default D-Bus will be used. .. py:classmethod:: new_proxy(bus, service_name, object_path) Create new proxy object and bypass ``__init__``. :param str service_name: - Remote object dbus connection name. + Remote object D-Bus connection name. For example, systemd uses ``org.freedesktop.systemd1`` :param str object_path: - Remote object dbus path. + Remote object D-Bus path. Should be a forward slash separated path. Starting object is usually ``/``. Example: ``/org/freedesktop/systemd/unit/dbus_2eservice`` :param SdBus bus: - Optional dbus connection object. - If not passed the default dbus will be used. + Optional D-Bus connection object. + If not passed the default D-Bus will be used. .. py:method:: export_to_dbus(object_path, bus) - Object will appear and become callable on dbus. + Object will appear and become callable on D-Bus. + + Returns a handle that can either be used as a context manager + to remove the object from D-Bus or ``.stop()`` method of the + handle can be called to remove object from D-Bus. + + Returns a handle that can be used to remove object from D-Bus + by either using it as a context manager or by calling ``.stop()`` + method of the handle. + + .. code-block:: python + + with dbus_object.export_to_dbus("/"): + # dbus_object can be called from D-Bus inside this + # with block. + ... + + ... + + handle = dbus_object2.export_to_dbus("/") + # dbus_object2 can be called from D-Bus between these statements + handle.stop() + + ... + + dbus_object3.export_to_dbus("/") + # dbus_object3 can be called from D-Bus until all references are + # dropped. + del dbus_object3 + + If the handle is discarded the object will remain exported until + it gets deallocated. + + *Changed in version 0.12.0:* Added a handle return. :param str object_path: Object path that it will be available at. :param SdBus bus: - Optional dbus connection object. - If not passed the default dbus will be used. + Optional D-Bus connection object. + If not passed the default D-Bus will be used. + + :return: Handle to control the export. .. py:class:: DbusObjectManagerInterfaceAsync(interface_name) @@ -147,47 +185,56 @@ Classes my_object_manager.export_to_dbus('/object/manager') managed_object = DbusInterfaceCommonAsync() - my_object_manager.export_with_manager('/object/manager/example') + my_object_manager.export_with_manager('/object/manager/example', managed_object) .. py:method:: get_managed_objects() :async: Get the objects this object manager in managing. + :py:func:`sdbus.utils.parse.parse_get_managed_objects` can be used + to make returned data easier to work with. + :return: Triple nested dictionary that contains all the objects paths with their properties values. - Dict[ObjectPath, Dict[InterfaceName, Dict[PropertyName, PropertyValue]]] + dict[ObjectPath, dict[InterfaceName, dict[PropertyName, PropertyValue]]] - :rtype: Dict[str, Dict[str, Dict[str, Any]]] + :rtype: dict[str, dict[str, dict[str, Any]]] .. py:attribute:: interfaces_added - :type: Tuple[str, Dict[str, Dict[str, Any]]] + :type: tuple[str, dict[str, dict[str, Any]]] Signal when a new object is added or and existing object gains a new interface. + :py:func:`sdbus.utils.parse.parse_interfaces_added` can be used + to make signal data easier to work with. + Signal data is: Object path : str Path to object that was added or modified. - Object interfaces and properties : Dict[str, Dict[str, Any]]] - Dict[InterfaceName, Dict[PropertyName, PropertyValue]] + Object interfaces and properties : dict[str, dict[str, Any]]] + dict[InterfaceName, dict[PropertyName, PropertyValue]] .. py:attribute:: interfaces_removed - :type: Tuple[str, List[str]] + :type: tuple[str, list[str]] Signal when existing object or and interface of existing object is removed. + :py:func:`sdbus.utils.parse.parse_interfaces_removed` can be used + to make signal data easier to work with. + Signal data is: Object path : str Path to object that was removed or modified. - Interfaces list : List[str] + Interfaces list : list[str] Interfaces names that were removed. .. py:method:: export_with_manager(object_path, object_to_export, bus) @@ -202,6 +249,36 @@ Classes ObjectManager will keep the reference to the object. + Returns a handle that can be used to remove object from D-Bus and + drop reference to it by either using it as a context manager or + by calling ``.stop()`` method of the handle. Signal will be emitted + once the object is stopped being exported. + + .. code-block:: python + + manager = DbusObjectManagerInterfaceAsync() + manager.export_to_dbus('/object/manager') + + with manager.export_with_manager("/object/manager/example", dbus_object): + # dbus_object can be called from D-Bus inside this + # with block. + ... + + # Removed signal will be emitted once the with block exits + + ... + + handle = manager.export_with_manager("/object/manager/example", dbus_object2) + # dbus_object2 can be called from D-Bus between these statements + handle.stop() + # Removed signal will be emitted once the .stop() method is called + + If the handle is discarded the object will remain exported until + it gets removed from manager with :py:meth:`remove_managed_object` and + the object gets deallocated. + + *Changed in version 0.12.0:* Added a handle return. + :param str object_path: Object path that it will be available at. @@ -209,10 +286,11 @@ Classes Object to export to D-Bus. :param SdBus bus: - Optional dbus connection object. - If not passed the default dbus will be used. + Optional D-Bus connection object. + If not passed the default D-Bus will be used. :raises RuntimeError: ObjectManager was not exported. + :return: Handle to control the export. .. py:method:: remove_managed_object(managed_object) @@ -239,11 +317,11 @@ Decorators Underlying function must be a coroutine function. - :param str input_signature: dbus input signature. + :param str input_signature: D-Bus input signature. Defaults to "" meaning method takes no arguments. Required if you intend to connect to a remote object. - :param str result_signature: dbus result signature. + :param str result_signature: D-Bus result signature. Defaults to "" meaning method returns empty reply on success. Required if you intend to serve the object. @@ -281,7 +359,7 @@ Decorators argument names will be used otherwise input arguments will be nameless - :param str method_name: Force specific dbus method name + :param str method_name: Force specific D-Bus method name instead of being based on Python function name. Example: :: @@ -300,7 +378,7 @@ Decorators result_signature='s', result_args_names=('uppercased', ) # This is optional but # makes arguments have names in - # instrospection data. + # introspection data. ) async def upper(self, str_to_up: str) -> str: return str_to_up.upper() @@ -309,7 +387,7 @@ Decorators .. py:decorator:: dbus_property_async(property_signature, [flags, [property_name]]) - Declare a dbus property. + Declare a D-Bus property. The underlying function has to be a regular ``def`` function. @@ -322,7 +400,7 @@ Decorators does not perform heavy IO or computation as that will block other methods or properties. - :param str property_signature: Property dbus signature. + :param str property_signature: Property D-Bus signature. Has to be a single type or container. :param int flags: modifies behavior. @@ -334,29 +412,6 @@ Decorators :param str property_name: Force specific property name instead of constructing it based on Python function name. - Properties have following methods: - - .. py:decoratormethod:: setter(set_function) - - Defines the setter function. - This makes the property read/write instead of read-only. - - See example on how to use. - - .. py:method:: get_async() - :async: - - Get the property value. - - The property can also be directly ``await`` ed - instead of calling this method. - - .. py:method:: set_async(new_value) - :async: - - Set property value. - - Example: :: from sdbus import DbusInterfaceCommonAsync, dbus_property_async @@ -386,13 +441,52 @@ Decorators def read_write_str_setter(self, new_str: str) -> None: self.s = new_str + .. py:class:: DbusPropertyAsync + + Properties have following methods: + + .. py:decoratormethod:: setter(set_function) + + Defines the setter function. + This makes the property read/write instead of read-only. + + See example on how to use. + + .. py:decoratormethod:: setter_private(set_function) + + Defines the private setter function. + The setter can be called locally but property + will be read-only from D-Bus. + + Calling the setter locally will emit + :py:attr:`properties_changed ` + signal to D-Bus. + + *Changed in version 0.12.0:* can now be used in overrides. + + .. py:method:: get_async() + :async: + + Get the property value. + + The property can also be directly ``await`` ed + instead of calling this method. + + .. py:method:: set_async(new_value) + :async: + + Set property value. + + + + .. py:decorator:: dbus_signal_async([signal_signature, [signal_args_names, [flags, [signal_name]]]]) - Defines a dbus signal. + Defines a D-Bus signal. Underlying function return type hint is used for signal type hints. - :param str signal_signature: signal dbus signature. + :param str signal_signature: signal D-Bus signature. Defaults to empty signal. :param Sequence[str] signal_args_names: sequence of signal argument names. @@ -416,58 +510,62 @@ Decorators :param str signal_name: Forces specific signal name instead of being based on Python function name. - Signals have following methods: + Example:: + + from sdbus import DbusInterfaceCommonAsync, dbus_signal_async - .. py:method:: catch() - Catch D-Bus signals using the async generator for loop: - ``async for x in something.some_signal.catch():`` + class ExampleInterface(DbusInterfaceCommonAsync, + interface_name='org.example.signal' + ): - This is main way to await for new events. + @dbus_signal_async('s') + def name_changed(self) -> str: + raise NotImplementedError - Both remote and local objects operate the same way. + .. py:class:: DbusSignalAsync - Signal objects can also be async iterated directly: - ``async for x in something.some_signal`` + Signals have following methods: - .. py:method:: catch_anywhere(service_name, bus) + .. py:method:: catch() - Catch signal independent of path. - Yields tuple of path of the object that emitted signal and signal data. + Catch D-Bus signals using the async generator for loop: + ``async for x in something.some_signal.catch():`` - ``async for path, data in something.some_signal.catch_anywhere():`` + This is main way to await for new events. - This method can be called from both an proxy object and class. - However, it cannot be called on local objects and will raise - ``NotImplementedError``. + Both remote and local objects operate the same way. - :param str service_name: - Service name of which signals belong to. - Required if called from class. When called from proxy object - the service name of the proxy will be used. + Signal objects can also be async iterated directly: + ``async for x in something.some_signal`` - :param str bus: - Optional dbus connection object. - If not passed when called from proxy the bus connected - to proxy will be used or when called from class default - bus will be used. + .. py:method:: catch_anywhere(service_name, bus) - .. py:method:: emit(args) + Catch signal independent of path. + Yields tuple of path of the object that emitted signal and signal data. - Emit a new signal with *args* data. + ``async for path, data in something.some_signal.catch_anywhere():`` - Example: :: + This method can be called from both an proxy object and class. + However, it cannot be called on local objects and will raise + ``NotImplementedError``. - from sdbus import DbusInterfaceCommonAsync, dbus_signal_async + :param str service_name: + Service name of which signals belong to. + Required if called from class. When called from proxy object + the service name of the proxy will be used. + :param str bus: + Optional D-Bus connection object. + If not passed when called from proxy the bus connected + to proxy will be used or when called from class default + bus will be used. + + .. py:method:: emit(args) + + Emit a new signal with *args* data. - class ExampleInterface(DbusInterfaceCommonAsync, - interface_name='org.example.signal' - ): - @dbus_signal_async('s') - def name_changed(self) -> str: - raise NotImplementedError .. py:decorator:: dbus_method_async_override() diff --git a/docs/asyncio_deep.rst b/docs/asyncio_deep.rst new file mode 100644 index 0000000..5e433d8 --- /dev/null +++ b/docs/asyncio_deep.rst @@ -0,0 +1,42 @@ +Asyncio advanced topics ++++++++++++++++++++++++++ + +.. py:currentmodule:: sdbus + +Signals without data +^^^^^^^^^^^^^^^^^^^^ + +D-Bus allows signals to not carry any data. Such signals have the +type signature of ``""``. (empty string) + +To emit such signals the :py:meth:`emit ` must +be explicitly called with ``None``. + +Example of an empty signal:: + + from asyncio import new_event_loop + from sdbus import DbusInterfaceCommonAsync, dbus_signal_async + + + class ExampleInterface( + DbusInterfaceCommonAsync, + interface_name="org.example.signal" + ): + + @dbus_signal_async("") + def name_invalidated(self) -> None: + raise NotImplementedError + + + test_object = ExampleInterface() + + + async def emit_empty_signal() -> None: + test_object.export_to_dbus("/") + + test_object.name_invalidated.emit(None) + + + loop = new_event_loop() + loop.run_until_complete(emit_empty_signal()) + diff --git a/docs/asyncio_quick.rst b/docs/asyncio_quick.rst index dac3c01..070f245 100644 --- a/docs/asyncio_quick.rst +++ b/docs/asyncio_quick.rst @@ -10,8 +10,8 @@ Python-sdbus works by declaring interface classes. Interface classes for async IO should be derived from :py:class:`DbusInterfaceCommonAsync`. -The class constructor takes ``interface_name`` keyword to determine the dbus interface name for all -dbus elements declared in the class body. +The class constructor takes ``interface_name`` keyword to determine the D-Bus interface name for all +D-Bus elements declared in the class body. Example: :: @@ -50,7 +50,7 @@ Example: :: # Signal with a list of strings @dbus_signal_async('as') - def str_signal(self) -> List[str]: + def str_signal(self) -> list[str]: raise NotImplementedError Initiating proxy @@ -77,14 +77,14 @@ Recommended to create proxy classes that a subclass of the interface: :: self._proxify('org.example.test', '/') -.. note:: Successfully initiating a proxy object does NOT guarantee that the dbus object exists. +.. note:: Successfully initiating a proxy object does NOT guarantee that the D-Bus object exists. Serving objects ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ :py:meth:`DbusInterfaceCommonAsync.export_to_dbus` method -will export the object to the dbus. After calling it the object -becomes visible on dbus for other processes to call. +will export the object to the D-Bus. After calling it the object +becomes visible on D-Bus for other processes to call. Example using ExampleInterface from before: :: @@ -135,7 +135,7 @@ Methods have to be async function, otherwise :py:exc:`AssertionError` will be ra While method calls are async there is a inherit timeout timer for any method call. -To return an error to caller you need to raise exception which has a :py:exc:`DbusFailedError` as base. +To return an error to caller you need to raise exception which has a :py:exc:`.DbusFailedError` as base. Regular exceptions will not propagate. See :doc:`/exceptions`. @@ -157,7 +157,7 @@ Example: :: result_signature='s', result_args_names=('uppercased', ) # This is optional but # makes arguments have names in - # instrospection data. + # introspection data. ) async def upper(self, str_to_up: str) -> str: return str_to_up.upper() @@ -238,7 +238,7 @@ Example: :: Signals ^^^^^^^^^^^^^^^^^^^^^^^^^^^ -To define a dbus signal wrap a function with :py:func:`dbus_signal_async` decorator. +To define a D-Bus signal wrap a function with :py:func:`dbus_signal_async` decorator. The function is only used for type hints information. It is recommended to just put ``raise NotImplementedError`` in to the body of the function. @@ -265,28 +265,28 @@ To catch a signal use ``async for`` loop: :: make sure to bind it to a variable and keep it referenced otherwise garbage collector will destroy your task. -A signal can be emitted with :py:meth:`emit` method. +A signal can be emitted with :py:meth:`emit ` method. -Example: :: +Example:: example_object.name_changed.emit('test') Signals can also be caught from multiple D-Bus objects using -:py:meth:`catch_anywhere` method. The async iterator will yield -the path of the object that emitted the signal and the signal data. +:py:meth:`catch_anywhere ` method. The async +iterator will yield the path of the object that emitted the signal and the signal data. -:py:meth:`catch_anywhere` can be called from class but in such case -the service name must be provided. +:py:meth:`catch_anywhere ` can be called from +class but in such case the service name must be provided. Example:: - async for path, x in ExampleInterface.name_changed('org.example.test'): + async for path, x in ExampleInterface.name_changed.catch_anywhere('org.example.test'): print(f"On {path} caught: {x}") Subclass Overrides ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -If you define a subclass which overrides a declared dbus method or property +If you define a subclass which overrides a declared D-Bus method or property you need to use :py:func:`dbus_method_async_override` and :py:func:`dbus_property_async_override` decorators. Overridden property can decorate a new setter. @@ -318,7 +318,7 @@ Example: :: Multiple interfaces ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -A dbus object can have multiple interfaces with different methods and properties. +A D-Bus object can have multiple interfaces with different methods and properties. To implement this define multiple interface classes and do a multiple inheritance on all interfaces the object has. @@ -342,7 +342,7 @@ Example: :: ): @dbus_method_async('as', 's') - async def join_str(self, str_array: List[str]) -> str: + async def join_str(self, str_array: list[str]) -> str: return ''.join(str_array) diff --git a/docs/autodoc.rst b/docs/autodoc.rst index accf77c..21d27ff 100644 --- a/docs/autodoc.rst +++ b/docs/autodoc.rst @@ -2,7 +2,7 @@ Autodoc extensions ================== Python-sdbus has an extension for Sphinx autodoc that can -document dbus interfaces. +document D-Bus interfaces. To use it include ``"sdbus.autodoc"`` extension in your ``conf.py`` file. @@ -12,7 +12,7 @@ To use it include ``"sdbus.autodoc"`` extension in your extensions = ['sdbus.autodoc'] The extension can document interface class bodies. For example, -`python-sdbus-networkmanager `_ +`python-sdbus-networkmanager `_ uses it to document the classes. .. code-block:: rst @@ -22,16 +22,16 @@ uses it to document the classes. .. warning:: Autodoc extension is early in development and has multiple issues. For example, the inheritance ``:inherited-members:`` - does not work on the dbus elements. + does not work on the D-Bus elements. Writing docstrings ------------------- -The dbus methods should be documented same way as the regular function +The D-Bus methods should be documented same way as the regular function would. See `Sphinx documentation on possible fields \ `_ -Example docstring for a dbus method: +Example docstring for a D-Bus method: .. code-block:: python @@ -45,14 +45,14 @@ Example docstring for a dbus method: """ raise NotImplementedError -Dbus properties and signals will be annotated with type taken from the +D-Bus properties and signals will be annotated with type taken from the stub function. .. code-block:: python @dbus_property_async('as') - def features(self) -> List[str]: - """List of dbus daemon features. + def features(self) -> list[str]: + """List of D-Bus daemon features. Features include: @@ -61,7 +61,7 @@ stub function. header fields. * 'SELinux' - Messages filtered by SELinux on this bus. * 'SystemdActivation' - services activated by systemd if their \ - .service file specifies a dbus name. + .service file specifies a D-Bus name. """ raise NotImplementedError diff --git a/docs/code_generator.rst b/docs/code_generator.rst index 950a53e..dfe31d5 100644 --- a/docs/code_generator.rst +++ b/docs/code_generator.rst @@ -3,15 +3,19 @@ Interface code generator Python-sdbus is able to generate the interfaces code from the D-Bus introspection XML. (either from a file or live object on D-Bus) -Currently only async interfaces code can be generated. +Currently async interfaces code is generated by default. +Blocking interfaces can be generated by passing ``--block`` option. Running code generator requires -`Jinja2 `_ +`Jinja `_ to be installed. .. warning:: Do NOT send the generator result to ``exec()`` function. Interface code MUST be inspected before running. +The generated interfaces code will be syntactically correct but NOT stylistically. +It is recommended running a code formatter on the generated code. (for example ``black``) + Generating from XML files ------------------------- @@ -47,3 +51,61 @@ Multiple object paths can be passed which generates a file containing all interfaces encountered in the objects. Pass ``--system`` option to use system bus instead of session bus. + +Renaming interfaces and members +------------------------------- + +*New in version 0.13.0.* + +Some interface and member names might conflict with Python keywords when +converted from D-Bus introspection to Python code by gerator. The CLI interface +allow to override the particular interface and member names using the ``--select-*`` +and ``--set-name`` options. The selector options move the cursor to a particular +interface and member + +Available override options: + +* ``--set-name`` + Sets the name of currently selected element as it would + be in generated Python code. Can be used if either interface or + member is selected. + +* ``--select-interface`` + Selects the interface using its D-Bus name. + +* ``--select-method`` + Selects the method using its D-Bus name. + An interface must be selected first. + +* ``--select-property`` + Selects the property using its D-Bus name. + An interface must be selected first. + +* ``--select-signal`` + Selects the signal using its D-Bus name. + An interface must be selected first. + +For example, an ``org.example.Interface`` interface has a property called ``Class``. +When automatically converted the name will become ``class`` which is a reserved Python keyword. + +Using these CLI options it is possible to override the name of the property and class: + +.. code-block:: shell + + python -m sdbus gen-from-file \ + org.example.interface.xml \ + --select-interface org.example.Interface \ + --set-name Example \ + --select-property Class \ + --set-name example_class + +This will generate following Python code: + +.. code-block:: python + + class Example: + @dbus_property_async( + property_signature="s", + ) + def example_class(self) -> str: + raise NotImplementedError diff --git a/docs/common_api.rst b/docs/common_api.rst index e61eea4..3c39f58 100644 --- a/docs/common_api.rst +++ b/docs/common_api.rst @@ -3,57 +3,37 @@ Common API These calls are shared between async and blocking API. -.. py:currentmodule:: sdbus - -Dbus connections calls -++++++++++++++++++++++++++++++++++ - -.. py:function:: request_default_bus_name_async(new_name) - :async: - - Acquire a name on the default bus async. - - :param str new_name: the name to acquire. - Must be a valid dbus service name. +Default bus ++++++++++++ -.. py:function:: request_default_bus_name(new_name) +.. automodule:: sdbus.default_bus + :members: - Acquire a name on the default bus. - - :param str new_name: the name to acquire. - Must be a valid dbus service name. - -.. py:function:: set_default_bus(new_default) - - Sets default bus. - - Should be called before you create any objects that might use - default bus. - - Default bus can be replaced but the change will only affect - newly created objects. +.. py:currentmodule:: sdbus - :param SdBus new_default: The bus object to set default to. +D-Bus connections calls ++++++++++++++++++++++++ -.. py:function:: get_default_bus(new_default) +.. py:function:: sd_bus_open() - Gets default bus. + Opens a new bus connection. The session bus will be opened + when available or system bus otherwise. - :return: default bus + :return: Session or system bus. :rtype: SdBus .. py:function:: sd_bus_open_user() Opens a new user session bus connection. - :return: session bus + :return: Session bus. :rtype: SdBus .. py:function:: sd_bus_open_system() Opens a new system bus connection. - :return: system bus + :return: System bus. :rtype: SdBus .. py:function:: sd_bus_open_system_remote(host) @@ -64,7 +44,7 @@ Dbus connections calls ``systemd-nspawn`` container name. :param str host: Host name to connect. - :return: Remote system bus + :return: Remote system bus. :rtype: SdBus .. py:function:: sd_bus_open_system_machine(machine) @@ -74,7 +54,7 @@ Dbus connections calls Special machine name ``.host`` indicates local system. :param str machine: Machine (container) name. - :return: Remote system bus + :return: Remote system bus. :rtype: SdBus .. py:function:: sd_bus_open_user_machine(machine) @@ -84,7 +64,7 @@ Dbus connections calls prefixed with ``username@`` for a specific user. :param str machine: Machine (container) name. - :return: Remote system bus + :return: Remote system bus. :rtype: SdBus Helper functions @@ -100,13 +80,13 @@ Helper functions :return: valid object path :rtype: str - Example on how systemd encodes unit names on dbus: :: + Example on how systemd encodes unit names on D-Bus: :: from sdbus import encode_object_path # System uses /org/freedesktop/systemd1/unit as prefix of all units - # dbus.service is a name of dbus unit but dot . is not a valid object path + # dbus.service is a name of D-Bus unit but dot . is not a valid object path s = encode_object_path('/org/freedesktop/systemd1/unit', 'dbus.service') print(s) # Prints: /org/freedesktop/systemd1/unit/dbus_2eservice diff --git a/docs/conf.py b/docs/conf.py index ab64486..b165939 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -25,9 +25,9 @@ author = 'igo95862' source_suffix = '.rst' extensions = ['sdbus.autodoc'] +html_theme = "sphinx_rtd_theme" autoclass_content = 'both' -autodoc_typehints = 'description' autodoc_member_order = 'bysource' path.insert(0, abspath('../src')) diff --git a/docs/examples.rst b/docs/examples.rst index 7ef60c2..22aa695 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -10,7 +10,7 @@ There are 3 files: * ``example_interface.py`` File that contains the interface definition. * ``example_server.py`` Server. -* ``example_interface.py`` Client. +* ``example_client.py`` Client. ``example_interface.py`` file: :: @@ -74,7 +74,7 @@ There are 3 files: # Acquire a known name on the bus # Clients will use that name to address to this server await request_default_bus_name_async('org.example.test') - # Export the object to dbus + # Export the object to D-Bus export_object.export_to_dbus('/') diff --git a/docs/exceptions.rst b/docs/exceptions.rst index d9e4cdf..35d2bd2 100644 --- a/docs/exceptions.rst +++ b/docs/exceptions.rst @@ -1,10 +1,12 @@ Exceptions ======================== +.. py:currentmodule:: sdbus.exceptions + Error name bound exceptions +++++++++++++++++++++++++++++++ -These exceptions are bound to specific dbus error names. For example, +These exceptions are bound to specific D-Bus error names. For example, :py:exc:`DbusFailedError` is bound to `org.freedesktop.DBus.Error.Failed` error name. @@ -80,6 +82,32 @@ Other exceptions Exception message contains line number and the error name. +.. _name-request-exceptions: + +Name request exceptions ++++++++++++++++++++++++ + +These exceptions will be raise if an error related to ownership of D-Bus +names occurs when calling :py:func:`.request_default_bus_name_async` or +:py:func:`.request_default_bus_name`. + +.. py:exception:: SdBusRequestNameError + + Common base exception for any name ownership error. + +.. py:exception:: SdBusRequestNameInQueueError + + Someone already owns the name but the request has been placed in queue. + +.. py:exception:: SdBusRequestNameExistsError + + Someone already owns the name. + +.. py:exception:: SdBusRequestNameAlreadyOwnerError + + The caller already owns the name. + + .. _list of error exceptions: Error name exception list @@ -202,7 +230,7 @@ Error name exception list No network access. - Encountered you use Dbus over TCP or SSH. + Encountered you use D-Bus over TCP or SSH. .. py:attribute:: dbus_error_name :type: str @@ -252,7 +280,7 @@ Error name exception list .. py:exception:: DbusUnknownMethodError - Unknown dbus method. + Unknown D-Bus method. .. py:attribute:: dbus_error_name :type: str @@ -260,7 +288,7 @@ Error name exception list .. py:exception:: DbusUnknownObjectError - Unknown dbus object. + Unknown D-Bus object. .. py:attribute:: dbus_error_name :type: str @@ -268,7 +296,7 @@ Error name exception list .. py:exception:: DbusUnknownInterfaceError - Unknown dbus interface. + Unknown D-Bus interface. .. py:attribute:: dbus_error_name :type: str @@ -276,7 +304,7 @@ Error name exception list .. py:exception:: DbusUnknownPropertyError - Unknown dbus property. + Unknown D-Bus property. .. py:attribute:: dbus_error_name :type: str @@ -284,7 +312,7 @@ Error name exception list .. py:exception:: DbusPropertyReadOnlyError - Dbus property is read only. + D-Bus property is read only. .. py:attribute:: dbus_error_name :type: str @@ -300,7 +328,7 @@ Error name exception list .. py:exception:: DbusInvalidSignatureError - Invalid dbus type signature. + Invalid D-Bus type signature. .. py:attribute:: dbus_error_name :type: str @@ -316,7 +344,7 @@ Error name exception list .. py:exception:: DbusInconsistentMessageError - Dbus message is malformed. + D-Bus message is malformed. .. py:attribute:: dbus_error_name :type: str diff --git a/docs/general.rst b/docs/general.rst index d4c72be..1a61eec 100644 --- a/docs/general.rst +++ b/docs/general.rst @@ -17,15 +17,15 @@ Asyncio is a part of python standard library that allows non-blocking io. `Asyncio documentation `_ Generally blocking IO should only be used for simple scripts and programs that interact -with existing dbus objects. +with existing D-Bus objects. Blocking: ^^^^^^^^^^^^^^^^^^^^^ * Blocking is easier to initiate (no event loop) * Properties behave exactly as Python properties do. (i.e. can assign with '=' operator) * Only allows one request at a time. -* No dbus signals. -* Cannot serve objects, only interact with existing object on dbus. +* No D-Bus signals. +* Cannot serve objects, only interact with existing object on D-Bus. :doc:`/sync_quick` @@ -35,8 +35,8 @@ Asyncio: ^^^^^^^^^^^^^^^^^^^^^^^^ * Calls need to be ``await`` ed. * Multiple requests at the same time. -* Serve object on dbus for other programs. -* Dbus Signals. +* Serve object on D-Bus for other programs. +* D-Bus Signals. :doc:`/asyncio_quick` @@ -44,12 +44,12 @@ Asyncio: .. _dbus-types: -Dbus types conversion +D-Bus types conversion ++++++++++++++++++++++++ -`Dbus types reference `_ +`D-Bus types reference `_ -.. note:: Python integers are unlimited size but dbus integers are not. +.. note:: Python integers are unlimited size but D-Bus integers are not. All integer types raise :py:exc:`OverflowError` if you try to pass number outside the type size. @@ -58,79 +58,79 @@ Dbus types conversion Signed integers range is ``-(2**(bit_size-1)) < (2**(bit_size-1))-1``. -+-------------+----------+-----------------+--------------------------------------------------------------------+ -| Name | Dbus type| Python type | Description | -+=============+==========+=================+====================================================================+ -| Boolean | b | :py:obj:`bool` | :py:obj:`True` or :py:obj:`False` | -+-------------+----------+-----------------+--------------------------------------------------------------------+ -| Byte | y | :py:obj:`int` | Unsigned 8-bit integer. | -| | | | **Note:** array of bytes (*ay*) has different type | -| | | | in python domain. | -+-------------+----------+-----------------+--------------------------------------------------------------------+ -| Int16 | n | :py:obj:`int` | Signed 16-bit integer. | -+-------------+----------+-----------------+--------------------------------------------------------------------+ -| Uint16 | q | :py:obj:`int` | Unsigned 16-bit integer. | -+-------------+----------+-----------------+--------------------------------------------------------------------+ -| Int32 | i | :py:obj:`int` | Signed 32-bit integer. | -+-------------+----------+-----------------+--------------------------------------------------------------------+ -| Uint32 | u | :py:obj:`int` | Unsigned 32-bit integer. | -+-------------+----------+-----------------+--------------------------------------------------------------------+ -| Int64 | x | :py:obj:`int` | Signed 64-bit integer. | -+-------------+----------+-----------------+--------------------------------------------------------------------+ -| Uint64 | t | :py:obj:`int` | Unsigned 64-bit integer. | -+-------------+----------+-----------------+--------------------------------------------------------------------+ -| Double | d | :py:obj:`float` | Float point number | -+-------------+----------+-----------------+--------------------------------------------------------------------+ -| Unix FD | h | :py:obj:`int` | File descriptor | -+-------------+----------+-----------------+--------------------------------------------------------------------+ -| String | s | :py:obj:`str` | String | -+-------------+----------+-----------------+--------------------------------------------------------------------+ -| Object | o | :py:obj:`str` | Syntactically correct dbus object path | -| Path | | | | -+-------------+----------+-----------------+--------------------------------------------------------------------+ -| Signature | g | :py:obj:`str` | Dbus type signature | -+-------------+----------+-----------------+--------------------------------------------------------------------+ -| Array | a | :py:obj:`list` | List of some single type. | -| | | | | -| | | | Example: ``as`` array of strings | -+-------------+----------+-----------------+--------------------------------------------------------------------+ -| Byte Array | ay | :py:obj:`bytes` | Array of bytes. Not a unique type in dbus but a different type in | -| | | | Python. Accepts both :py:obj:`bytes` and :py:obj:`bytearray`. | -| | | | Used for binary data. | -+-------------+----------+-----------------+--------------------------------------------------------------------+ -| Struct | () | :py:obj:`tuple` | Tuple. | -| | | | | -| | | | Example: ``(isax)`` tuple of int, string and array of int. | -+-------------+----------+-----------------+--------------------------------------------------------------------+ -| Dictionary | a{} | :py:obj:`dict` | Dictionary with key type and value type. | -| | | | | -| | | | **Note:** Dictionary is always a part of array. | -| | | | I.E. ``a{si}`` is the dict with string keys and integer values. | -| | | | ``{si}`` is NOT a valid signature. | -+-------------+----------+-----------------+--------------------------------------------------------------------+ -| Variant | v | :py:obj:`tuple` | Unknown type that can be any single type. | -| | | | In Python represented by a tuple of | -| | | | a signature string and a single type. | -| | | | | -| | | | Example: ``("s", "test")`` variant of a single string | -+-------------+----------+-----------------+--------------------------------------------------------------------+ ++-------------+------------+-----------------+--------------------------------------------------------------------+ +| Name | D-Bus type | Python type | Description | ++=============+============+=================+====================================================================+ +| Boolean | b | :py:obj:`bool` | :py:obj:`True` or :py:obj:`False` | ++-------------+------------+-----------------+--------------------------------------------------------------------+ +| Byte | y | :py:obj:`int` | Unsigned 8-bit integer. | +| | | | **Note:** array of bytes (*ay*) has different type | +| | | | in python domain. | ++-------------+------------+-----------------+--------------------------------------------------------------------+ +| Int16 | n | :py:obj:`int` | Signed 16-bit integer. | ++-------------+------------+-----------------+--------------------------------------------------------------------+ +| Uint16 | q | :py:obj:`int` | Unsigned 16-bit integer. | ++-------------+------------+-----------------+--------------------------------------------------------------------+ +| Int32 | i | :py:obj:`int` | Signed 32-bit integer. | ++-------------+------------+-----------------+--------------------------------------------------------------------+ +| Uint32 | u | :py:obj:`int` | Unsigned 32-bit integer. | ++-------------+------------+-----------------+--------------------------------------------------------------------+ +| Int64 | x | :py:obj:`int` | Signed 64-bit integer. | ++-------------+------------+-----------------+--------------------------------------------------------------------+ +| Uint64 | t | :py:obj:`int` | Unsigned 64-bit integer. | ++-------------+------------+-----------------+--------------------------------------------------------------------+ +| Double | d | :py:obj:`float` | Float point number | ++-------------+------------+-----------------+--------------------------------------------------------------------+ +| Unix FD | h | :py:obj:`int` | File descriptor | ++-------------+------------+-----------------+--------------------------------------------------------------------+ +| String | s | :py:obj:`str` | String | ++-------------+------------+-----------------+--------------------------------------------------------------------+ +| Object | o | :py:obj:`str` | Syntactically correct D-Bus object path | +| Path | | | | ++-------------+------------+-----------------+--------------------------------------------------------------------+ +| Signature | g | :py:obj:`str` | D-Bus type signature | ++-------------+------------+-----------------+--------------------------------------------------------------------+ +| Array | a | :py:obj:`list` | List of some single type. | +| | | | | +| | | | Example: ``as`` array of strings | ++-------------+------------+-----------------+--------------------------------------------------------------------+ +| Byte Array | ay | :py:obj:`bytes` | Array of bytes. Not a unique type in D-Bus but a different type in | +| | | | Python. Accepts both :py:obj:`bytes` and :py:obj:`bytearray`. | +| | | | Used for binary data. | ++-------------+------------+-----------------+--------------------------------------------------------------------+ +| Struct | () | :py:obj:`tuple` | Tuple. | +| | | | | +| | | | Example: ``(isax)`` tuple of int, string and array of int. | ++-------------+------------+-----------------+--------------------------------------------------------------------+ +| Dictionary | a{} | :py:obj:`dict` | Dictionary with key type and value type. | +| | | | | +| | | | **Note:** Dictionary is always a part of array. | +| | | | I.E. ``a{si}`` is the dict with string keys and integer values. | +| | | | ``{si}`` is NOT a valid signature. | ++-------------+------------+-----------------+--------------------------------------------------------------------+ +| Variant | v | :py:obj:`tuple` | Unknown type that can be any single type. | +| | | | In Python represented by a tuple of | +| | | | a signature string and a single type. | +| | | | | +| | | | Example: ``("s", "test")`` variant of a single string | ++-------------+------------+-----------------+--------------------------------------------------------------------+ Name conversions +++++++++++++++++++++ -Dbus uses CamelCase for method names. +D-Bus uses CamelCase for method names. Python uses snake_case. When decorating a method name will be automatically translated from snake_case to CamelCase. Example: ``close_notification`` -> ``CloseNotification`` -However, all decorators have a parameter to force Dbus name to a specific value. +However, all decorators have a parameter to force D-Bus name to a specific value. See API documentation for a particular decorator. Default bus -++++++++++++++++++++++++++ ++++++++++++ Most object methods that take a bus as a parameter will use a thread-local default bus connection if a bus object @@ -139,29 +139,35 @@ is not explicitly passed. Session bus is default bus when running as a user and system bus otherwise. -:py:func:`request_default_bus_name_async` can be used to acquire -a service name on default bus. +The :py:func:`request_default_bus_name_async ` +and :py:func:`request_default_bus_name ` +can be used to acquire a service name on the default bus. Use :py:func:`sd_bus_open_user` and :py:func:`sd_bus_open_system` to acquire a specific bus connection. -Set the default connection to a new default with :py:func:`set_default_bus`. -This should be done before any object that take bus as an init argument are created. +The :py:func:`set_default_bus ` can be used to set the new +thread-local bus. This should be done before any objects that take bus as +an init argument are created. If no bus has been set the new bus will +be initialized and set as thread-local default. -In the future there will be a better way to create and acquire -new bus connections. +The bus can also be set as default for the current context using +:py:func:`set_context_default_bus `. +The context refers to the standard library's ``contextvars`` module context variables +frequently used in asyncio frameworks. Context-local default bus has higher priority over +thread-local default bus. Glossary +++++++++++++++++++++ -* **Bus** object representing connection to dbus. -* **Proxy** Python object that represents an object on DBus. +* **Bus** object representing connection to D-Bus. +* **Proxy** Python object that represents an object on D-Bus. Without proxy you manipulate messages directly. * **Remote** something that exists outside current Python process. * **Local** something that exists inside current Python scope. -* **Service Name** a well known name that an process can acquire on dbus. +* **Service Name** a well known name that an process can acquire on D-Bus. For example, systemd acquires ``org.freedesktop.systemd1`` name. -* **Signature** dbus type definition. Represented by a string. See :ref:`dbus-types`. +* **Signature** D-Bus type definition. Represented by a string. See :ref:`dbus-types`. Contents ++++++++++++++++++++ diff --git a/docs/index.rst b/docs/index.rst index 0c8a3dd..21b5646 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,30 +1,30 @@ Welcome to Python-sdbus documentation! ======================================================= -Python-sdbus is the python dbus library that aim to use the modern features of python +Python-sdbus is the python D-Bus library that aim to use the modern features of python * `Asyncio `_ * `Type hints `_ * `Based on fast sd-bus `_ * Unified client/server interface classes. Write interface class once. -* Dbus methods can have keyword and default arguments. +* D-Bus methods can have keyword and default arguments. D-Bus ----------- D-Bus is the inter-process communication standard commonly used on Linux desktop. -This documentation expects you to be familiar with dbus concepts and conventions. +This documentation expects you to be familiar with D-Bus concepts and conventions. If you are unfamiliar with D-Bus you might want to read following pages: `Wikipedia page `_ -`Lennart Poettering post about dbus `_ +`Lennart Poettering post about D-Bus `_ -`Dbus specification by freedesktop `_ +`D-Bus specification by freedesktop.org `_ -`Install D-Feet D-Bus debugger and observe services and objects on your dbus `_ +`Install D-Spy D-Bus debugger and observe services and objects on your D-Bus `_ .. toctree:: @@ -37,7 +37,9 @@ If you are unfamiliar with D-Bus you might want to read following pages: sync_api asyncio_quick asyncio_api + asyncio_deep exceptions + utils examples proxies code_generator diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 0000000..88760ba --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,3 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# Copyright (C) 2023 igo95862 +sphinx_rtd_theme diff --git a/docs/sync_api.rst b/docs/sync_api.rst index a3ce8e8..641949e 100644 --- a/docs/sync_api.rst +++ b/docs/sync_api.rst @@ -8,11 +8,11 @@ Classes .. py:class:: DbusInterfaceCommon(interface_name) - Dbus interface class. - Dbus methods and properties should be defined using + D-Bus interface class. + D-Bus methods and properties should be defined using :py:func:`dbus_property` and :py:func:`dbus_method` decorators. - :param str interface_name: Sets the dbus interface + :param str interface_name: Sets the D-Bus interface name that will be used for all properties and methods defined in the body of the class @@ -21,22 +21,22 @@ Classes Init will create a proxy to a remote object :param str service_name: - Remote object dbus connection name. + Remote object D-Bus connection name. For example, systemd uses ``org.freedesktop.systemd1`` :param str object_path: - Remote object dbus path. + Remote object D-Bus path. Should be a forward slash separated path. Starting object is usually ``/``. Example: ``/org/freedesktop/systemd/unit/dbus_2eservice`` :param SdBus bus: - Optional dbus connection object. - If not passed the default dbus will be used. + Optional D-Bus connection object. + If not passed the default D-Bus will be used. .. py:method:: dbus_ping() - Pings the remote service using dbus. + Pings the remote service using D-Bus. Useful to test if connection or remote service is alive. @@ -52,7 +52,7 @@ Classes .. py:method:: dbus_introspect() - Get dbus introspection XML. + Get D-Bus introspection XML. It is users responsibility to parse that data. @@ -72,7 +72,7 @@ Classes either raise an ``"error"`` (default), ``"ignore"`` the property or ``"reuse"`` the D-Bus name for the member. :return: dictionary of properties - :rtype: Dict[str, Any] + :rtype: dict[str, Any] Example: :: @@ -91,12 +91,12 @@ Classes # Method that does not take any arguments and returns a list of str @dbus_method() - def get_capabilities(self) -> List[str]: + def get_capabilities(self) -> list[str]: raise NotImplementedError # Method that takes a dict of {str: str} and returns an int @dbus_method('a{ss}') - def count_entries(self, a_dict: Dict[str, str]) -> int: + def count_entries(self, a_dict: dict[str, str]) -> int: raise NotImplementedError # Read only property of int @@ -124,21 +124,21 @@ Classes Triple nested dictionary that contains all the objects paths with their properties values. - Dict[ObjectPath, Dict[InterfaceName, Dict[PropertyName, PropertyValue]]] + dict[ObjectPath, dict[InterfaceName, dict[PropertyName, PropertyValue]]] - :rtype: Dict[str, Dict[str, Dict[str, Any]]] + :rtype: dict[str, dict[str, dict[str, Any]]] Decorators +++++++++++++++ .. py:decorator:: dbus_method([input_signature, [flags, [method_name]]]) - - Define dbus method - Decorated function becomes linked to dbus method. + Define D-Bus method + + Decorated function becomes linked to D-Bus method. Always use round brackets () even when not passing any arguments. - :param str input_signature: dbus input signature. + :param str input_signature: D-Bus input signature. Defaults to "" meaning method takes no arguments. Required if method takes any arguments. @@ -168,12 +168,12 @@ Decorators # Method that does not take any arguments and returns a list of str @dbus_method() - def get_capabilities(self) -> List[str]: + def get_capabilities(self) -> list[str]: raise NotImplementedError # Method that takes a dict of {str: str} and returns an int @dbus_method('a{ss}') - def count_entries(self, a_dict: Dict[str, str]) -> int: + def count_entries(self, a_dict: dict[str, str]) -> int: raise NotImplementedError Calling methods example:: @@ -193,16 +193,16 @@ Decorators .. py:decorator:: dbus_property([property_signature, [flags, [property_name]]]) - Define dbus property + Define D-Bus property Property works just like @property decorator would. Always use round brackets () even when not passing any arguments. - Read only property can be indicated by passing empty dbus signature "". + Read only property can be indicated by passing empty D-Bus signature "". Trying to assign a read only property will raise :py:exc:`AttributeError` - :param str property_signature: dbus property signature. + :param str property_signature: D-Bus property signature. Empty signature "" indicates read-only property. Defaults to empty signature "". Required only for writable properties. diff --git a/docs/sync_quick.rst b/docs/sync_quick.rst index 6c9e695..23f0ccc 100644 --- a/docs/sync_quick.rst +++ b/docs/sync_quick.rst @@ -10,8 +10,8 @@ Python-sdbus works by declaring interface classes. Interface classes for blocking IO should be derived from :py:class:`DbusInterfaceCommon`. -The class constructor takes ``interface_name`` keyword to determine the dbus interface name for all -dbus elements declared in the class body. +The class constructor takes ``interface_name`` keyword to determine the D-Bus interface name for all +D-Bus elements declared in the class body. Example:: @@ -42,13 +42,13 @@ Example:: def test_int(self) -> int: raise NotImplementedError -This is an interface of that defines a one dbus method and one property. +This is an interface of that defines a one D-Bus method and one property. The actual body of the decorated function will not be called. Instead the call will be routed -through dbus to a another process. Interface can have non-decorated functions that will act +through D-Bus to a another process. Interface can have non-decorated functions that will act as regular methods. -Blocking IO can only interact with existing dbus objects and can not be +Blocking IO can only interact with existing D-Bus objects and can not be served for other processes to interact with. See :ref:`blocking-vs-async` Initiating proxy @@ -68,7 +68,7 @@ Example creating a proxy and calling method:: d.close_notification(1234) -.. note:: Successfully initiating a proxy object does NOT guarantee that the dbus object +.. note:: Successfully initiating a proxy object does NOT guarantee that the D-Bus object exists. Methods @@ -76,7 +76,7 @@ Methods Methods are functions wrapped with :py:func:`dbus_method` decorator. -If the remote object sends an error reply an exception with base of :py:exc:`DbusFailedError` +If the remote object sends an error reply an exception with base of :py:exc:`.DbusFailedError` will be raised. See :doc:`/exceptions` for list of exceptions. The wrapped function will not be called. Its recommended to set the function to ``raise NotImplementedError``. @@ -98,7 +98,7 @@ Example: :: Properties ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -DBus property is defined by wrapping a function with :py:func:`dbus_property` decorator. +D-Bus property is defined by wrapping a function with :py:func:`dbus_property` decorator. Example: :: @@ -128,12 +128,12 @@ The new property behaves very similar to Pythons :py:func:`property` decorator. # Assign new string d.test_string = 'some_string' -If property is read-only when :py:exc:`DbusPropertyReadOnlyError` will be raised. +If property is read-only when :py:exc:`.DbusPropertyReadOnlyError` will be raised. Multiple interfaces ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -A dbus object can have multiple interfaces with different methods and properties. +A D-Bus object can have multiple interfaces with different methods and properties. To implement this define multiple interface classes and do a multiple inheritance on all interfaces the object has. @@ -157,7 +157,7 @@ Example: :: ): @dbus_method('as') - def test_method(self, str_array: List[str]) -> None: + def test_method(self, str_array: list[str]) -> None: raise NotImplementedError diff --git a/docs/unittest.rst b/docs/unittest.rst index d7f5364..ab0a200 100644 --- a/docs/unittest.rst +++ b/docs/unittest.rst @@ -16,6 +16,43 @@ Python-sdbus provides several utilities to enable unit testing. Requires ``dbus-daemon`` executable be installed. + Example:: + + from sdbus import DbusInterfaceCommonAsync, dbus_method_async + from sdbus.unittest import IsolatedDbusTestCase + + class TestInterface(DbusInterfaceCommonAsync, + interface_name='org.test.test', + ): + + @dbus_method_async("s", "s") + async def upper(self, string: str) -> str: + """Uppercase the input""" + return string.upper() + + def initialize_object() -> tuple[TestInterface, TestInterface]: + test_object = TestInterface() + test_object.export_to_dbus('/') + + test_object_connection = TestInterface.new_proxy( + "org.example.test", '/') + + return test_object, test_object_connection + + + class TestProxy(IsolatedDbusTestCase): + async def asyncSetUp(self) -> None: + await super().asyncSetUp() + await self.bus.request_name_async("org.example.test", 0) + + async def test_method_kwargs(self) -> None: + test_object, test_object_connection = initialize_object() + + self.assertEqual( + 'TEST', + await test_object_connection.upper('test'), + ) + .. py:attribute:: bus :type: SdBus @@ -23,40 +60,32 @@ Python-sdbus provides several utilities to enable unit testing. It is also set as a default bus. + .. py:method:: assertDbusSignalEmits(signal, timeout=1) + + Assert that a given signal was emitted at least once within the + given timeout. + + :param signal: D-Bus signal object. Can be a signal from either local or proxy object. + :param Union[int, float] timeout: Maximum wait time until first captured signal. -Usage example: :: + Should be used as an async context manager. The context manager exits as soon + as first signal is captured. - from sdbus import DbusInterfaceCommonAsync, dbus_method_async - from sdbus.unittest import IsolatedDbusTestCase + The object returned by context manager has following attributes: - class TestInterface(DbusInterfaceCommonAsync, - interface_name='org.test.test', - ): + .. py:attribute:: output + :type: list[Any] - @dbus_method_async("s", "s") - async def upper(self, string: str) -> str: - """Uppercase the input""" - return string.upper() + List of captured data. - def initialize_object() -> Tuple[TestInterface, TestInterface]: - test_object = TestInterface() - test_object.export_to_dbus('/') + Example:: - test_object_connection = TestInterface.new_proxy( - "org.example.test", '/') + async with self.assertDbusSignalEmits(test_object.test_signal) as signal_record: + test_object.test_signal.emit("test") - return test_object, test_object_connection + self.assertEqual(["test"], signal_record.output) + *New in version 0.12.0.* - class TestProxy(IsolatedDbusTestCase): - async def asyncSetUp(self) -> None: - await super().asyncSetUp() - await self.bus.request_name_async("org.example.test", 0) - async def test_method_kwargs(self) -> None: - test_object, test_object_connection = initialize_object() - self.assertEqual( - 'TEST', - await test_object_connection.upper('test'), - ) diff --git a/docs/utils.rst b/docs/utils.rst new file mode 100644 index 0000000..f04ccde --- /dev/null +++ b/docs/utils.rst @@ -0,0 +1,21 @@ +Utilities +========= + +Parsing utilities ++++++++++++++++++ + +Parse unweildy D-Bus structures in to Python native objects and names. +Available under ``sdbus.utils.parse`` subpackage. + +.. automodule:: sdbus.utils.parse + :members: + +Inspect utilities ++++++++++++++++++ + +Inspect D-Bus objects and retrieve their D-Bus related attributes +such as D-Bus object paths and etc... +Available under ``sdbus.utils.inspect`` subpackage. + +.. automodule:: sdbus.utils.inspect + :members: diff --git a/examples/simple/client.py b/examples/simple/client.py index c541996..59a0ca8 100644 --- a/examples/simple/client.py +++ b/examples/simple/client.py @@ -46,7 +46,7 @@ async def get_hello_world() -> None: loop = new_event_loop() -# Always binds your tasks to a variable +# Always bind your tasks to a variable task_upper = loop.create_task(call_upper()) task_clock = loop.create_task(print_clock()) task_hello_world = loop.create_task(get_hello_world()) diff --git a/examples/simple/client_blocking.py b/examples/simple/client_blocking.py new file mode 100644 index 0000000..453f13f --- /dev/null +++ b/examples/simple/client_blocking.py @@ -0,0 +1,64 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later + +# Copyright (C) 2025 igo95862 + +# This file is part of python-sdbus + +# 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 __future__ import annotations + +from sdbus import DbusInterfaceCommon, dbus_method, dbus_property + +# The interface has to be redefined using the blocking base class +# and decorators. + + +class ExampleInterfaceBlocking( + DbusInterfaceCommon, interface_name="org.example.interface" +): + @dbus_method( + input_signature="s", + result_signature="s", + ) + def upper(self, string: str) -> str: + return string.upper() + + @dbus_property( + property_signature="s", + ) + def hello_world(self) -> str: + return "Hello, World!" + + +def main() -> None: + # Create a new proxied object + example_object = ExampleInterfaceBlocking( + service_name="org.example.test", + object_path="/", + ) + + # Call upper + s = "test string" + s_after = example_object.upper(s) + + print("Initial string: ", s) + print("After call: ", s_after) + + # Get property + print("Remote property: ", example_object.hello_world) + + +if __name__ == "__main__": + main() diff --git a/examples/simple/server.py b/examples/simple/server.py index b5306a7..cc7d73d 100644 --- a/examples/simple/server.py +++ b/examples/simple/server.py @@ -48,7 +48,7 @@ async def startup() -> None: # Acquire a known name on the bus # Clients will use that name to address this server await request_default_bus_name_async('org.example.test') - # Export the object to dbus + # Export the object to D-Bus export_object.export_to_dbus('/') diff --git a/lgtm.yml b/lgtm.yml deleted file mode 100644 index 3a2ccc6..0000000 --- a/lgtm.yml +++ /dev/null @@ -1,6 +0,0 @@ ---- - -extraction: - python: - python_setup: - setup_py: "./setup.py" diff --git a/meson.build b/meson.build index 7590d6b..b1ccea6 100644 --- a/meson.build +++ b/meson.build @@ -43,4 +43,3 @@ else endif subdir('src') -subdir('wheel-build') diff --git a/setup.py b/setup.py index a3ad61f..199b300 100644 --- a/setup.py +++ b/setup.py @@ -22,11 +22,11 @@ from os import environ from subprocess import DEVNULL, PIPE from subprocess import run as subprocess_run -from typing import List, Optional, Tuple +from typing import Optional from setuptools import Extension, setup -c_macros: List[Tuple[str, Optional[str]]] = [] +c_macros: list[tuple[str, Optional[str]]] = [] def get_libsystemd_version() -> int: @@ -35,11 +35,14 @@ def get_libsystemd_version() -> int: stderr=DEVNULL, stdout=PIPE, check=True, + text=True, ) - result_str = process.stdout.decode('utf-8') + result_str = process.stdout + # Version can either be like 250 or 250.10 + first_component = result_str.split(".")[0] - return int(result_str) + return int(first_component) if not environ.get('PYTHON_SDBUS_USE_IGNORE_SYSTEMD_VERSION'): @@ -52,7 +55,7 @@ def get_libsystemd_version() -> int: c_macros.append(('LIBSYSTEMD_NO_OPEN_USER_MACHINE', None)) -def get_link_arguments() -> List[str]: +def get_link_arguments() -> list[str]: process = subprocess_run( args=('pkg-config', '--libs-only-l', 'libsystemd'), stderr=DEVNULL, @@ -65,7 +68,7 @@ def get_link_arguments() -> List[str]: return result_str.rstrip(' \n').split(' ') -link_arguments: List[str] = get_link_arguments() +link_arguments: list[str] = get_link_arguments() if environ.get('PYTHON_SDBUS_USE_STATIC_LINK'): # Link statically against libsystemd and libcap @@ -74,12 +77,12 @@ def get_link_arguments() -> List[str]: link_arguments.append('-flto') -compile_arguments: List[str] = ['-flto'] +compile_arguments: list[str] = ['-flto'] use_limited_api = False if environ.get('PYTHON_SDBUS_USE_LIMITED_API'): - c_macros.append(('Py_LIMITED_API', '0x03070000')) + c_macros.append(('Py_LIMITED_API', '0x03090000')) use_limited_api = True @@ -93,16 +96,16 @@ def get_link_arguments() -> List[str]: 'Based on sd-bus from libsystemd.'), long_description=long_description, long_description_content_type='text/markdown', - version='0.10.2.1', - url='https://github.com/igo95862/python-sdbus', + version='0.14.2', + url='https://github.com/python-sdbus/python-sdbus', author='igo95862', author_email='igo95862@yandex.ru', license='LGPL-2.1-or-later', keywords='dbus ipc linux freedesktop', project_urls={ 'Documentation': 'https://python-sdbus.readthedocs.io/en/latest/', - 'Source': 'https://github.com/igo95862/python-sdbus/', - 'Tracker': 'https://github.com/igo95862/python-sdbus/issues/', + 'Source': 'https://github.com/python-sdbus/python-sdbus/', + 'Tracker': 'https://github.com/python-sdbus/python-sdbus/issues/', }, classifiers=[ 'Development Status :: 4 - Beta', @@ -115,10 +118,12 @@ def get_link_arguments() -> List[str]: 'Programming Language :: Python :: 3 :: Only', 'Topic :: Software Development :: Libraries :: Python Modules', ], - packages=['sdbus', - # 'sdbus_async', 'sdbus_block', - 'sdbus_async.dbus_daemon', 'sdbus_block.dbus_daemon', - ], + packages=[ + 'sdbus', + 'sdbus.utils', + 'sdbus_async.dbus_daemon', + 'sdbus_block.dbus_daemon', + ], package_dir={ 'sdbus': 'src/sdbus', 'sdbus_async.dbus_daemon': 'src/sdbus_async/dbus_daemon', @@ -137,7 +142,7 @@ def get_link_arguments() -> List[str]: 'py.typed', ], }, - python_requires='>=3.7', + python_requires='>=3.9', ext_modules=[ Extension( 'sdbus.sd_bus_internals', diff --git a/src/sdbus/__init__.py b/src/sdbus/__init__.py index 28583f9..0e0f7b8 100644 --- a/src/sdbus/__init__.py +++ b/src/sdbus/__init__.py @@ -17,13 +17,8 @@ # 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 __future__ import annotations -from .dbus_common_funcs import ( - get_default_bus, - request_default_bus_name, - request_default_bus_name_async, - set_default_bus, -) from .dbus_exceptions import ( DbusAccessDeniedError, DbusAddressInUseError, @@ -57,15 +52,13 @@ DbusUnknownObjectError, DbusUnknownPropertyError, ) -from .dbus_proxy_async_interfaces import ( - DbusInterfaceCommonAsync, - DbusObjectManagerInterfaceAsync, -) +from .dbus_proxy_async_interfaces import DbusInterfaceCommonAsync from .dbus_proxy_async_method import ( dbus_method_async, dbus_method_async_override, get_current_message, ) +from .dbus_proxy_async_object_manager import DbusObjectManagerInterfaceAsync from .dbus_proxy_async_property import ( dbus_property_async, dbus_property_async_override, @@ -77,6 +70,13 @@ ) from .dbus_proxy_sync_method import dbus_method from .dbus_proxy_sync_property import dbus_property +from .default_bus import ( + get_default_bus, + request_default_bus_name, + request_default_bus_name_async, + set_context_default_bus, + set_default_bus, +) from .sd_bus_internals import ( DbusDeprecatedFlag, DbusHiddenFlag, @@ -103,9 +103,6 @@ ) __all__ = ( - 'get_default_bus', 'request_default_bus_name', - 'request_default_bus_name_async', 'set_default_bus', - 'DbusAccessDeniedError', 'DbusAddressInUseError', 'DbusAuthFailedError', 'DbusBadAddressError', 'DbusDisconnectedError', 'DbusFailedError', @@ -146,6 +143,12 @@ 'dbus_property', + "get_default_bus", + "request_default_bus_name", + "request_default_bus_name_async", + "set_context_default_bus", + "set_default_bus", + 'DbusDeprecatedFlag', 'DbusHiddenFlag', 'DbusNoReplyFlag', diff --git a/src/sdbus/__main__.py b/src/sdbus/__main__.py index 7081468..d9ad8ae 100644 --- a/src/sdbus/__main__.py +++ b/src/sdbus/__main__.py @@ -19,70 +19,370 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA from __future__ import annotations -from argparse import ArgumentParser, Namespace +from argparse import SUPPRESS, Action, ArgumentParser +from dataclasses import dataclass, field from pathlib import Path -from typing import List +from sys import stdout +from typing import TYPE_CHECKING from .interface_generator import ( - DbusInterfaceIntrospection, - generate_async_py_file, + generate_py_file, interfaces_from_file, interfaces_from_str, ) +if TYPE_CHECKING: + from typing import Optional -def run_gen_from_connection(namespace: Namespace) -> None: - connection_name = namespace.connection_name - object_paths = namespace.object_paths + from .interface_generator import DbusInterfaceIntrospection + + +@dataclass +class RenameMember: + new_name: Optional[str] = None + current_arg: Optional[str] = None + arg_renames: dict[str, str] = field(default_factory=dict) + + +@dataclass +class RenameInterface: + new_name: Optional[str] = None + current_member: Optional[RenameMember] = None + methods: dict[str, RenameMember] = field(default_factory=dict) + properties: dict[str, RenameMember] = field(default_factory=dict) + signals: dict[str, RenameMember] = field(default_factory=dict) + + +@dataclass +class RenameRoot: + current_interface: Optional[RenameInterface] = None + interfaces: dict[str, RenameInterface] = field(default_factory=dict) + + +rename_root = RenameRoot() + + +# def rename_args(member_rename): +# ... + + +def rename_members( + interface: DbusInterfaceIntrospection, + interface_rename: RenameInterface, +) -> None: + for m_member in interface.methods: + m_rename = interface_rename.methods.get(m_member.method_name) + if m_rename is None: + continue + + if m_rename.new_name is not None: + m_member.python_name = m_rename.new_name + + for p_member in interface.properties: + p_rename = interface_rename.properties.get(p_member.method_name) + if p_rename is None: + continue + + if p_rename.new_name is not None: + p_member.python_name = p_rename.new_name + + for s_member in interface.signals: + s_rename = interface_rename.signals.get(s_member.method_name) + if s_rename is None: + continue + + if s_rename.new_name is not None: + s_member.python_name = s_rename.new_name + + +def rename_interfaces( + interfaces: list[DbusInterfaceIntrospection] +) -> None: + for interface in interfaces: + dbus_interface_name = interface.interface_name + this_interface_rename = rename_root.interfaces.get(dbus_interface_name) + if this_interface_rename is None: + continue + + if this_interface_rename.new_name is not None: + interface.python_name = this_interface_rename.new_name + + rename_members(interface, this_interface_rename) + + +def run_gen_from_connection( + connection_name: str, + object_paths: list[str], + system: bool, + imports_header: bool, + do_async: bool, +) -> None: + connection_name = connection_name + object_paths = object_paths from .dbus_proxy_sync_interfaces import DbusInterfaceCommon - if namespace.system: - from .dbus_common_funcs import set_default_bus + if system: + from .default_bus import set_default_bus from .sd_bus_internals import sd_bus_open_system set_default_bus(sd_bus_open_system()) - interfaces: List[DbusInterfaceIntrospection] = [] + interfaces: list[DbusInterfaceIntrospection] = [] for object_path in object_paths: connection = DbusInterfaceCommon(connection_name, object_path) itrospection = connection.dbus_introspect() interfaces.extend(interfaces_from_str(itrospection)) - print( - generate_async_py_file( - interfaces, namespace.no_imports_header)) + rename_interfaces(interfaces) + + stdout.write( + generate_py_file( + interfaces, + imports_header, + do_async, + ) + ) -def run_gen_from_file(namespace: Namespace) -> None: - interfaces: List[DbusInterfaceIntrospection] = [] +def run_gen_from_file( + filenames: list[str], + imports_header: bool, + do_async: bool, +) -> None: + interfaces: list[DbusInterfaceIntrospection] = [] - for file in namespace.filenames: + for file in filenames: interfaces.extend(interfaces_from_file(file)) - print( - generate_async_py_file( - interfaces, namespace.no_imports_header)) + rename_interfaces(interfaces) + stdout.write( + generate_py_file( + interfaces, + imports_header, + do_async, + ) + ) -def generator_main() -> None: - main_arg_parser = ArgumentParser() - subparsers = main_arg_parser.add_subparsers() +class ActionSelectInterface(Action): + def __call__( + self, + parser: ArgumentParser, + namespace: object, + values: object, + option_string: Optional[str] = None, + ) -> None: + if not isinstance(values, str): + raise TypeError( + f"Expected --select-interface to be string, got {values!r}" + ) - generate_from_file_parser = subparsers.add_parser('gen-from-file') - generate_from_file_parser.set_defaults(func=run_gen_from_file) + interface_rename = rename_root.interfaces.get(values) - generate_from_file_parser.add_argument( - 'filenames', type=Path, nargs='+') + if interface_rename is None: + interface_rename = RenameInterface() + rename_root.interfaces[values] = interface_rename - generate_from_file_parser.add_argument( - '--no-imports-header', action='store_false', default=True, - help="Do NOT include 'import' header", + rename_root.current_interface = interface_rename + + +class ActionSelectMethod(Action): + def __call__( + self, + parser: ArgumentParser, + namespace: object, + values: object, + option_string: Optional[str] = None, + ) -> None: + if not isinstance(values, str): + raise TypeError( + f"Expected --select-method to be string, got {values!r}" + ) + + current_interface = rename_root.current_interface + if current_interface is None: + raise ValueError( + "No D-Bus interface selected. " + "Use --select-interface option." + ) + + method_rename = current_interface.methods.get(values) + + if method_rename is None: + method_rename = RenameMember() + current_interface.methods[values] = method_rename + + current_interface.current_member = method_rename + + +class ActionSelectProperty(Action): + def __call__( + self, + parser: ArgumentParser, + namespace: object, + values: object, + option_string: Optional[str] = None, + ) -> None: + if not isinstance(values, str): + raise TypeError( + f"Expected --select-property to be string, got {values!r}" + ) + + current_interface = rename_root.current_interface + if current_interface is None: + raise ValueError( + "No D-Bus interface selected. " + "Use --select-interface option." + ) + + property_rename = current_interface.properties.get(values) + + if property_rename is None: + property_rename = RenameMember() + current_interface.properties[values] = property_rename + + current_interface.current_member = property_rename + + +class ActionSelectSignal(Action): + def __call__( + self, + parser: ArgumentParser, + namespace: object, + values: object, + option_string: Optional[str] = None, + ) -> None: + if not isinstance(values, str): + raise TypeError( + f"Expected --select-signal to be string, got {values!r}" + ) + + current_interface = rename_root.current_interface + if current_interface is None: + raise ValueError( + "No D-Bus interface selected. " + "Use --select-interface option." + ) + + signal_rename = current_interface.signals.get(values) + + if signal_rename is None: + signal_rename = RenameMember() + current_interface.signals[values] = signal_rename + + current_interface.current_member = signal_rename + + +class ActionSetName(Action): + def __call__( + self, + parser: ArgumentParser, + namespace: object, + values: object, + option_string: Optional[str] = None, + ) -> None: + if not isinstance(values, str): + raise TypeError( + f"Expected --set-name to be string, got {values!r}" + ) + + current_interface = rename_root.current_interface + current_member = ( + current_interface.current_member + if current_interface is not None + else None + ) + + if current_member is not None: + current_member.new_name = values + return + + if current_interface is not None: + current_interface.new_name = values + return + + raise ValueError( + "No D-Bus element to rename. " + "Use --select-* options to select element." + ) + + +def generator_main(args: Optional[list[str]] = None) -> None: + + main_arg_parser = ArgumentParser( + prog="sdbus", + ) + subparsers = main_arg_parser.add_subparsers( + required=True, + title="subcommands", ) + generate_from_file_parser = subparsers.add_parser('gen-from-file') + generate_from_file_parser.set_defaults(func=run_gen_from_file) + generate_from_connection = subparsers.add_parser('gen-from-connection') generate_from_connection.set_defaults(func=run_gen_from_connection) + # Common options + for subparser in (generate_from_file_parser, generate_from_connection): + subparser.add_argument( + "--no-imports-header", action="store_false", + dest="imports_header", + help="Do NOT include 'import' header", + ) + subparser.add_argument( + "--imports-header", action="store_true", default=True, + dest="imports_header", + help="Include 'import' header (default)", + ) + + subparser.add_argument( + "--async", action="store_true", default=True, + dest="do_async", + help="Generate async interfaces (default)", + ) + subparser.add_argument( + "--block", action="store_false", + dest="do_async", + help="Generate blocking interfaces", + ) + subparser.add_argument( + "--select-interface", + action=ActionSelectInterface, + default=SUPPRESS, + help="Select D-Bus interface to adjust" + ) + subparser.add_argument( + "--select-method", + action=ActionSelectMethod, + default=SUPPRESS, + help="Select D-Bus method to adjust" + ) + subparser.add_argument( + "--select-property", + action=ActionSelectProperty, + default=SUPPRESS, + help="Select D-Bus property to adjust" + ) + subparser.add_argument( + "--select-signal", + action=ActionSelectSignal, + default=SUPPRESS, + help="Select D-Bus signal to adjust" + ) + subparser.add_argument( + "--set-name", + action=ActionSetName, + default=SUPPRESS, + help="Select D-Bus interface to adjust" + ) + + generate_from_file_parser.add_argument( + 'filenames', type=Path, nargs='+', + help="Paths to interface XML introspection files" + ) + generate_from_connection.add_argument( 'connection_name', help=( @@ -98,18 +398,15 @@ def generator_main() -> None: 'One or more.' ) ) - generate_from_connection.add_argument( - '--no-imports-header', action='store_false', default=True, - help="Do NOT include 'import' header", - ) generate_from_connection.add_argument( '--system', help='Use system D-Bus instead of session.', action='store_true', ) - args = main_arg_parser.parse_args() - args.func(args) + args_dict = vars(main_arg_parser.parse_args(args)) + func = args_dict.pop("func") + func(**args_dict) if __name__ == "__main__": diff --git a/src/sdbus/autodoc.py b/src/sdbus/autodoc.py index 8a709ea..8b4a918 100644 --- a/src/sdbus/autodoc.py +++ b/src/sdbus/autodoc.py @@ -17,38 +17,36 @@ # 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 __future__ import annotations -from typing import Any, Dict +from typing import TYPE_CHECKING -from sphinx.application import Sphinx from sphinx.ext.autodoc import AttributeDocumenter, MethodDocumenter -from .dbus_proxy_async_method import DbusMethodAsyncBinded -from .dbus_proxy_async_property import ( - DbusPropertyAsync, - DbusPropertyAsyncBinded, -) -from .dbus_proxy_async_signal import DbusSignalAsync, DbusSignalBinded +from .dbus_proxy_async_method import DbusMethodAsync +from .dbus_proxy_async_property import DbusPropertyAsync +from .dbus_proxy_async_signal import DbusSignalAsync + +if TYPE_CHECKING: + from typing import Any + + from sphinx.application import Sphinx class DbusMethodDocumenter(MethodDocumenter): - objtype = 'DbusMethodAsyncBinded' + objtype = 'DbusMethodAsyncClassBind' directivetype = 'method' priority = 100 + MethodDocumenter.priority @classmethod def can_document_member(cls, member: Any, *args: Any) -> bool: - return isinstance(member, DbusMethodAsyncBinded) + return isinstance(member, DbusMethodAsync) def import_object(self, raiseerror: bool = False) -> bool: - self.objpath.append('dbus_method') self.objpath.append('original_method') ret = super().import_object(raiseerror) self.objpath.pop() - self.objpath.pop() return ret def add_content(self, @@ -65,20 +63,13 @@ def add_content(self, class DbusPropertyDocumenter(AttributeDocumenter): - objtype = 'DbusPropertyAsyncBinded' + objtype = 'DbusPropertyAsync' directivetype = 'attribute' priority = 100 + AttributeDocumenter.priority @classmethod def can_document_member(cls, member: Any, *args: Any) -> bool: - return isinstance(member, DbusPropertyAsyncBinded) - - def import_object(self, raiseerror: bool = False) -> bool: - - self.objpath.append('dbus_property') - ret = super().import_object(raiseerror) - self.objpath.pop() - return ret + return isinstance(member, DbusPropertyAsync) def add_content(self, *args: Any, **kwargs: Any, @@ -106,20 +97,13 @@ def add_content(self, class DbusSignalDocumenter(AttributeDocumenter): - objtype = 'DbusSignalBinded' + objtype = 'DbusSignalAsync' directivetype = 'attribute' priority = 100 + AttributeDocumenter.priority @classmethod def can_document_member(cls, member: Any, *args: Any) -> bool: - return isinstance(member, DbusSignalBinded) - - def import_object(self, raiseerror: bool = False) -> bool: - - self.objpath.append('dbus_signal') - ret = super().import_object(raiseerror) - self.objpath.pop() - return ret + return isinstance(member, DbusSignalAsync) def add_content(self, *args: Any, **kwargs: Any, @@ -145,7 +129,7 @@ def add_content(self, super().add_content(*args, **kwargs) -def setup(app: Sphinx) -> Dict[str, bool]: +def setup(app: Sphinx) -> dict[str, bool]: app.setup_extension('sphinx.ext.autodoc') app.add_autodocumenter(DbusMethodDocumenter) app.add_autodocumenter(DbusPropertyDocumenter) diff --git a/src/sdbus/dbus_common_elements.py b/src/sdbus/dbus_common_elements.py index 2dd19e9..4df138f 100644 --- a/src/sdbus/dbus_common_elements.py +++ b/src/sdbus/dbus_common_elements.py @@ -20,59 +20,78 @@ from __future__ import annotations from inspect import getfullargspec -from types import FunctionType -from typing import ( - Any, - Callable, - Dict, - List, - Optional, - Sequence, - Tuple, - TypeVar, -) +from typing import TYPE_CHECKING, Generic, TypeVar from .dbus_common_funcs import ( _is_property_flags_correct, - _method_name_converter, + snake_case_to_camel_case, ) +from .default_bus import get_default_bus from .sd_bus_internals import is_interface_name_valid, is_member_name_valid +if TYPE_CHECKING: + from asyncio import Task + from collections.abc import Callable, Sequence + from types import FunctionType + from typing import Any, Optional + + SelfMeta = TypeVar('SelfMeta', bound="DbusInterfaceMetaCommon") + + from .sd_bus_internals import SdBus, SdBusInterface + +T = TypeVar('T') -class DbusSomethingCommon: - def __init__(self) -> None: - self.interface_name: Optional[str] = None - self.serving_enabled: bool = True +class DbusMemberCommon: + interface_name: str + serving_enabled: bool -class DbusSomethingAsync(DbusSomethingCommon): + +class DbusMemberAsync(DbusMemberCommon): ... -class DbusSomethingSync(DbusSomethingCommon): +class DbusMemberSync(DbusMemberCommon): ... class DbusInterfaceMetaCommon(type): - def __new__(cls, name: str, - bases: Tuple[type, ...], - namespace: Dict[str, Any], + def __new__(cls: type[SelfMeta], name: str, + bases: tuple[type, ...], + namespace: dict[str, Any], interface_name: Optional[str] = None, serving_enabled: bool = True, - ) -> DbusInterfaceMetaCommon: + ) -> SelfMeta: if interface_name is not None: try: assert is_interface_name_valid(interface_name), ( f"Invalid interface name: \"{interface_name}\"; " 'Interface names must be composed of 2 or more elements ' 'separated by a dot \'.\' character. All elements must ' - 'contain at least one character, constist of ASCII ' + 'contain at least one character, consist of ASCII ' 'characters, first character must not be digit and ' 'length must not exceed 255 characters.' ) except NotImplementedError: ... + for attr_name, attr in namespace.items(): + if not isinstance(attr, DbusMemberCommon): + continue + + # TODO: Fix async metaclass copying all methods + if hasattr(attr, "interface_name"): + continue + + if interface_name is None: + raise TypeError( + f"Defined D-Bus element {attr_name!r} without " + f"interface name in the class {name!r}." + ) + + attr.interface_name = interface_name + attr.serving_enabled = serving_enabled + new_cls = super().__new__(cls, name, bases, namespace) return new_cls @@ -86,16 +105,16 @@ def __new__(cls, name: str, ) -class DbusMethodCommon(DbusSomethingCommon): +class DbusMethodCommon(DbusMemberCommon): def __init__( self, original_method: FunctionType, method_name: Optional[str], input_signature: str, - input_args_names: Sequence[str], + input_args_names: Optional[Sequence[str]], result_signature: str, - result_args_names: Sequence[str], + result_args_names: Optional[Sequence[str]], flags: int): assert not isinstance(input_args_names, str), ( @@ -103,17 +122,8 @@ def __init__( " names. Did you forget to put" " it in to a tuple ('string', ) ?") - assert not any(' ' in x for x in input_args_names), ( - "Can't have spaces in argument input names" - f"Args: {input_args_names}") - - assert not any(' ' in x for x in result_args_names), ( - "Can't have spaces in argument result names." - f"Args: {result_args_names}") - if method_name is None: - method_name = ''.join( - _method_name_converter(original_method.__name__)) + method_name = snake_case_to_camel_case(original_method.__name__) try: assert is_member_name_valid(method_name), ( @@ -136,13 +146,25 @@ def __init__( self.method_name = method_name self.input_signature = input_signature - self.input_args_names: Sequence[str] = ( - self.args_names - if result_args_names and not input_args_names - else input_args_names) + self.input_args_names: Sequence[str] = () + if input_args_names is not None: + assert not any(' ' in x for x in input_args_names), ( + "Can't have spaces in argument input names" + f"Args: {input_args_names}") + + self.input_args_names = input_args_names + elif result_args_names is not None: + self.input_args_names = self.args_names self.result_signature = result_signature - self.result_args_names = result_args_names + self.result_args_names: Sequence[str] = () + if result_args_names is not None: + assert not any(' ' in x for x in result_args_names), ( + "Can't have spaces in argument result names." + f"Args: {result_args_names}") + + self.result_args_names = result_args_names + self.flags = flags self.__doc__ = original_method.__doc__ @@ -151,7 +173,7 @@ def _rebuild_args( self, function: FunctionType, *args: Any, - **kwargs: Dict[str, Any]) -> List[Any]: + **kwargs: dict[str, Any]) -> list[Any]: # 3 types of arguments # *args - should be passed directly # **kwargs - should be put in a proper order @@ -174,7 +196,7 @@ def _rebuild_args( passed_args_iter = iter(args) default_args_iter = iter(self.args_defaults) - new_args_list: List[Any] = [] + new_args_list: list[Any] = [] for i, a_name in enumerate(self.args_spec.args[1:]): try: @@ -201,15 +223,14 @@ def _rebuild_args( return new_args_list -class DbusPropertyCommon(DbusSomethingCommon): +class DbusPropertyCommon(DbusMemberCommon): def __init__(self, property_name: Optional[str], property_signature: str, flags: int, original_method: FunctionType): if property_name is None: - property_name = ''.join( - _method_name_converter(original_method.__name__)) + property_name = snake_case_to_camel_case(original_method.__name__) try: assert is_member_name_valid(property_name), ( @@ -232,7 +253,7 @@ def __init__(self, self.flags = flags -class DbusSingalCommon(DbusSomethingCommon): +class DbusSignalCommon(DbusMemberCommon): def __init__(self, signal_name: Optional[str], signal_signature: str, @@ -240,8 +261,7 @@ def __init__(self, flags: int, original_method: FunctionType): if signal_name is None: - signal_name = ''.join( - _method_name_converter(original_method.__name__)) + signal_name = snake_case_to_camel_case(original_method.__name__) try: assert is_member_name_valid(signal_name), ( @@ -261,21 +281,75 @@ def __init__(self, self.__annotations__ = original_method.__annotations__ -class DbusBindedAsync: +class DbusBoundAsync: ... -class DbusBindedSync: +class DbusBoundSync: ... -T = TypeVar('T') +class DbusMethodOverride(Generic[T]): + def __init__(self, override_method: T): + self.override_method = override_method -class DbusOverload: - def __init__(self, original: T): - self.original = original - self.setter_overload: Optional[Callable[[Any, T], None]] = None +class DbusPropertyOverride(Generic[T]): + def __init__(self, getter_override: Callable[[Any], T]): + self.getter_override = getter_override + self.setter_override: Optional[Callable[[Any, T], None]] = None + self.is_setter_public = True def setter(self, new_setter: Optional[Callable[[Any, T], None]]) -> None: - self.setter_overload = new_setter + self.setter_override = new_setter + + def setter_private( + self, + new_setter: Optional[Callable[[Any, T], None]], + ) -> None: + self.setter_override = new_setter + self.is_setter_public = False + + +class DbusRemoteObjectMeta: + def __init__( + self, + service_name: str, + object_path: str, + bus: Optional[SdBus] = None, + ): + self.service_name = service_name + self.object_path = object_path + self.attached_bus = ( + bus if bus is not None + else get_default_bus() + ) + + +class DbusLocalObjectMeta: + def __init__(self) -> None: + self.activated_interfaces: list[SdBusInterface] = [] + self.serving_object_path: Optional[str] = None + self.attached_bus: Optional[SdBus] = None + self._tasks: Optional[set[Task[None]]] = None + + @property + def tasks(self) -> set[Task[None]]: + tasks_set = self._tasks + if tasks_set is None: + tasks_set = set() + self._tasks = tasks_set + + return tasks_set + + +class DbusClassMeta: + def __init__( + self, + interface_name: str, + serving_enabled: bool, + ) -> None: + self.interface_name = interface_name + self.serving_enabled = serving_enabled + self.dbus_member_to_python_attr: dict[str, str] = {} + self.python_attr_to_dbus_member: dict[str, str] = {} diff --git a/src/sdbus/dbus_common_funcs.py b/src/sdbus/dbus_common_funcs.py index 486cad7..08a7f4c 100644 --- a/src/sdbus/dbus_common_funcs.py +++ b/src/sdbus/dbus_common_funcs.py @@ -1,7 +1,6 @@ - # SPDX-License-Identifier: LGPL-2.1-or-later -# Copyright (C) 2020-2022 igo95862 +# Copyright (C) 2020-2023 igo95862 # This file is part of python-sdbus @@ -21,19 +20,19 @@ from __future__ import annotations from asyncio import get_running_loop -from contextvars import ContextVar -from typing import Iterator +from typing import TYPE_CHECKING from .sd_bus_internals import ( DbusPropertyConstFlag, DbusPropertyEmitsChangeFlag, DbusPropertyEmitsInvalidationFlag, DbusPropertyExplicitFlag, - SdBus, - sd_bus_open, ) -DEFAULT_BUS: ContextVar[SdBus] = ContextVar('DEFAULT_BUS') +if TYPE_CHECKING: + from collections.abc import Iterator, Mapping + from typing import Any, Literal + PROPERTY_FLAGS_MASK = ( DbusPropertyConstFlag | DbusPropertyEmitsChangeFlag | @@ -50,35 +49,8 @@ def _is_property_flags_correct(flags: int) -> bool: return (0 <= num_of_flag_bits <= 1) -def get_default_bus() -> SdBus: - try: - return DEFAULT_BUS.get() - except LookupError: - new_bus = sd_bus_open() - DEFAULT_BUS.set(new_bus) - return new_bus - - -def set_default_bus(new_default: SdBus) -> None: - DEFAULT_BUS.set(new_default) - - -async def request_default_bus_name_async( - new_name: str, - flags: int = 0,) -> None: - default_bus = get_default_bus() - await default_bus.request_name_async(new_name, flags) - - -async def request_default_bus_name( - new_name: str, - flags: int = 0,) -> None: - default_bus = get_default_bus() - default_bus.request_name(new_name, flags) - - -def _method_name_converter(python_name: str) -> Iterator[str]: - char_iter = iter(python_name) +def _snake_case_to_camel_case_gen(snake: str) -> Iterator[str]: + char_iter = iter(snake) # Name starting with upper case letter try: first_char = next(char_iter) @@ -100,9 +72,39 @@ def _method_name_converter(python_name: str) -> Iterator[str]: upper_next_one = True +def snake_case_to_camel_case(snake: str) -> str: + return "".join(_snake_case_to_camel_case_gen(snake)) + + def _check_sync_in_async_env() -> bool: try: get_running_loop() return False except RuntimeError: return True + + +def _parse_properties_vardict( + properties_name_map: Mapping[str, str], + properties_vardict: dict[str, tuple[str, Any]], + on_unknown_member: Literal['error', 'ignore', 'reuse'], +) -> dict[str, Any]: + + properties_translated: dict[str, Any] = {} + + for member_name, variant in properties_vardict.items(): + try: + python_name = properties_name_map[member_name] + except KeyError: + if on_unknown_member == 'error': + raise + elif on_unknown_member == 'ignore': + continue + elif on_unknown_member == 'reuse': + python_name = member_name + else: + raise ValueError + + properties_translated[python_name] = variant[1] + + return properties_translated diff --git a/src/sdbus/dbus_exceptions.py b/src/sdbus/dbus_exceptions.py index afc9358..b178503 100644 --- a/src/sdbus/dbus_exceptions.py +++ b/src/sdbus/dbus_exceptions.py @@ -19,7 +19,7 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA from __future__ import annotations -from typing import Any, Dict, Tuple, cast +from typing import TYPE_CHECKING from .sd_bus_internals import ( SdBusBaseError, @@ -27,22 +27,30 @@ map_exception_to_dbus_error, ) +if TYPE_CHECKING: + from typing import Any + class DbusErrorMeta(type): - def __new__(cls, name: str, - bases: Tuple[type, ...], - namespace: Dict[str, Any], - ) -> DbusErrorMeta: + def __new__( + cls, + name: str, + bases: tuple[type, ...], + namespace: dict[str, Any], + ) -> type[Exception]: dbus_error_name = namespace.get('dbus_error_name') if dbus_error_name is None: - raise TypeError('Dbus error name not passed') + raise TypeError('D-Bus error name not passed') new_cls = super().__new__(cls, name, bases, namespace) + assert issubclass(new_cls, Exception), ( + f"New class {new_cls} is not an Exception but {bases}." + ) - add_exception_mapping(cast(Exception, new_cls)) + add_exception_mapping(new_cls) return new_cls diff --git a/src/sdbus/dbus_proxy_async_interface_base.py b/src/sdbus/dbus_proxy_async_interface_base.py index cc19dac..9fdf92a 100644 --- a/src/sdbus/dbus_proxy_async_interface_base.py +++ b/src/sdbus/dbus_proxy_async_interface_base.py @@ -19,158 +19,288 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA from __future__ import annotations -from asyncio import Queue -from copy import deepcopy -from inspect import getmembers -from types import MethodType -from typing import ( - Any, - Callable, - Dict, - List, - Optional, - Set, - Tuple, - Type, - TypeVar, - cast, -) +from collections.abc import Callable +from copy import copy +from itertools import chain +from typing import TYPE_CHECKING, Any, cast from warnings import warn -from weakref import ref as weak_ref +from weakref import WeakKeyDictionary, WeakValueDictionary from .dbus_common_elements import ( - DbusBindedAsync, + DbusClassMeta, DbusInterfaceMetaCommon, - DbusOverload, - DbusSomethingAsync, - DbusSomethingSync, + DbusLocalObjectMeta, + DbusMemberAsync, + DbusMemberCommon, + DbusMemberSync, + DbusMethodOverride, + DbusPropertyOverride, + DbusRemoteObjectMeta, ) -from .dbus_common_funcs import get_default_bus -from .dbus_proxy_async_method import DbusMethodAsync, DbusMethodAsyncBinded +from .dbus_proxy_async_method import DbusLocalMethodAsync, DbusMethodAsync from .dbus_proxy_async_property import ( + DbusLocalPropertyAsync, DbusPropertyAsync, - DbusPropertyAsyncBinded, ) -from .dbus_proxy_async_signal import DbusSignalAsync, DbusSignalBinded -from .sd_bus_internals import SdBus, SdBusInterface +from .dbus_proxy_async_signal import DbusLocalSignalAsync, DbusSignalAsync +from .default_bus import get_default_bus +from .sd_bus_internals import SdBusInterface + +if TYPE_CHECKING: + from collections.abc import Iterable, Iterator + from typing import Optional, TypeVar, Union + + from .sd_bus_internals import SdBus, SdBusSlot + + T = TypeVar('T') + Self = TypeVar('Self', bound="DbusInterfaceBaseAsync") + DbusOverride = Union[DbusMethodOverride[T], DbusPropertyOverride[T]] -T_input = TypeVar('T_input') + +DBUS_CLASS_TO_META: WeakKeyDictionary[ + type, DbusClassMeta] = WeakKeyDictionary() +DBUS_INTERFACE_NAME_TO_CLASS: WeakValueDictionary[ + str, DbusInterfaceMetaAsync] = WeakValueDictionary() class DbusInterfaceMetaAsync(DbusInterfaceMetaCommon): - def __new__(cls, name: str, - bases: Tuple[type, ...], - namespace: Dict[str, Any], - interface_name: Optional[str] = None, - serving_enabled: bool = True, - ) -> DbusInterfaceMetaAsync: - dbus_served_interfaces_names = ( - {interface_name} - if serving_enabled and interface_name is not None - else set() + @staticmethod + def _process_dbus_method_override( + override_attr_name: str, + override: DbusMethodOverride[T], + mro_dbus_elements: dict[str, DbusMemberAsync], + ) -> DbusMethodAsync: + try: + original_dbus_method = mro_dbus_elements[override_attr_name] + except KeyError: + raise ValueError( + f"No D-Bus method {override_attr_name!r} found " + f"to override." + ) + + if not isinstance(original_dbus_method, DbusMethodAsync): + raise TypeError( + f"Expected {DbusMethodAsync!r} got {original_dbus_method!r} " + f"under name {override_attr_name!r}" + ) + + new_method = copy(original_dbus_method) + new_method.original_method = ( + override.override_method # type: ignore[assignment] ) - dbus_to_python_name_map: Dict[str, str] = {} - dbus_declared_members: Dict[str, DbusSomethingAsync] = {} - superclass_members: Dict[str, DbusSomethingAsync] = {} - - for base in bases: - if issubclass(base, DbusInterfaceBaseAsync): - dbus_to_python_name_map.update( - base._dbus_to_python_name_map + return new_method + + @staticmethod + def _process_dbus_property_override( + override_attr_name: str, + override: DbusPropertyOverride[T], + mro_dbus_elements: dict[str, DbusMemberAsync], + ) -> DbusPropertyAsync[Any]: + try: + original_property = mro_dbus_elements[override_attr_name] + except KeyError: + raise ValueError( + f"No D-Bus property {override_attr_name!r} found " + f"to override." + ) + + if not isinstance(original_property, DbusPropertyAsync): + raise TypeError( + f"Expected {DbusMethodAsync!r} got {original_property!r} " + f"under name {override_attr_name!r}" + ) + + new_property = copy(original_property) + new_property.property_getter = cast( + Callable[[DbusInterfaceBaseAsync], Any], + override.getter_override + ) + if override.setter_override is not None: + new_property.property_setter = override.setter_override + new_property.property_setter_is_public = override.is_setter_public + + return new_property + + @classmethod + def _check_collisions( + cls, + new_class_name: str, + namespace: dict[str, Any], + mro_dbus_elements: dict[str, DbusMemberAsync], + ) -> None: + + possible_collisions = namespace.keys() & mro_dbus_elements.keys() + new_overrides: dict[str, DbusMemberAsync] = {} + + for attr_name, attr in namespace.items(): + if isinstance(attr, DbusMethodOverride): + new_overrides[attr_name] = cls._process_dbus_method_override( + attr_name, + attr, + mro_dbus_elements, ) - dbus_served_interfaces_names.update( - base._dbus_served_interfaces_names + possible_collisions.remove(attr_name) + elif isinstance(attr, DbusPropertyOverride): + new_overrides[attr_name] = cls._process_dbus_property_override( + attr_name, + attr, + mro_dbus_elements, ) + possible_collisions.remove(attr_name) + else: + continue + + if possible_collisions: + raise ValueError( + f"Interface {new_class_name!r} redefines reserved " + f"D-Bus attribute names: {possible_collisions!r}" + ) - superclass_members.update( - base._dbus_declared_members + namespace.update(new_overrides) + + @staticmethod + def _extract_dbus_elements( + dbus_class: type, + dbus_meta: DbusClassMeta, + ) -> dict[str, DbusMemberAsync]: + dbus_elements_map: dict[str, DbusMemberAsync] = {} + + for attr_name in dbus_meta.python_attr_to_dbus_member.keys(): + dbus_element = dbus_class.__dict__.get(attr_name) + if not isinstance(dbus_element, DbusMemberAsync): + raise TypeError( + f"Expected async D-Bus element, got {dbus_element!r} " + f"in class {dbus_class!r}" ) - for key, value in namespace.items(): - assert not isinstance(value, DbusSomethingSync), ( - "Can't mix sync methods in async interface." + dbus_elements_map[attr_name] = dbus_element + + return dbus_elements_map + + @classmethod + def _map_mro_dbus_elements( + cls, + new_class_name: str, + base_classes: Iterable[type], + ) -> dict[str, DbusMemberAsync]: + all_python_dbus_map: dict[str, DbusMemberAsync] = {} + possible_collisions: set[str] = set() + + for c in base_classes: + dbus_meta = DBUS_CLASS_TO_META.get(c) + if dbus_meta is None: + continue + + base_dbus_elements = cls._extract_dbus_elements(c, dbus_meta) + + possible_collisions.update( + base_dbus_elements.keys() & all_python_dbus_map.keys() ) - if isinstance(value, DbusSomethingAsync): - value.interface_name = interface_name - value.serving_enabled = serving_enabled - dbus_declared_members[key] = value - - if isinstance(value, DbusMethodAsync): - dbus_to_python_name_map[value.method_name] = key - elif isinstance(value, DbusPropertyAsync): - dbus_to_python_name_map[value.property_name] = key - elif isinstance(value, DbusSignalAsync): - dbus_to_python_name_map[value.signal_name] = key - - try: - super_dbus_def = superclass_members[key] - except KeyError: - if isinstance(value, DbusOverload): - raise TypeError( - f"No D-Bus member '{key}' to overload with." - ) - else: - if not isinstance(value, DbusOverload): - raise TypeError( - "Attempted to overload dbus definition" - " without using @dbus_overload decorator" - ) + all_python_dbus_map.update( + base_dbus_elements + ) - if isinstance(super_dbus_def, DbusMethodAsync): - new_method_def = deepcopy(super_dbus_def) - new_method_def.original_method = cast( - MethodType, value.original) - - namespace[key] = new_method_def - elif isinstance(super_dbus_def, DbusPropertyAsync): - new_property_def = deepcopy(super_dbus_def) - new_property_def.property_getter = cast( - Callable[[DbusInterfaceBaseAsync], Any], - value.original) - if value.setter_overload is not None: - new_property_def.property_setter = ( - value.setter_overload - ) - - namespace[key] = new_property_def - else: - raise TypeError('Unknown D-Bus overload') + if possible_collisions: + raise ValueError( + f"Interface {new_class_name!r} has a reserved D-Bus " + f"attribute name collision: {possible_collisions!r}" + ) + + return all_python_dbus_map - dbus_declared_members.update(superclass_members) + @staticmethod + def _map_dbus_elements( + attr_name: str, + attr: Any, + meta: DbusClassMeta, + interface_name: str, + ) -> None: + if not isinstance(attr, DbusMemberCommon): + return + + if isinstance(attr, DbusMemberSync): + raise TypeError( + "Can't mix blocking methods in " + f"async interface: {attr_name!r}" + ) + + if attr.interface_name != interface_name: + return + + if isinstance(attr, DbusMethodAsync): + meta.dbus_member_to_python_attr[attr.method_name] = attr_name + meta.python_attr_to_dbus_member[attr_name] = attr.method_name + elif isinstance(attr, DbusPropertyAsync): + meta.dbus_member_to_python_attr[attr.property_name] = attr_name + meta.python_attr_to_dbus_member[attr_name] = attr.property_name + elif isinstance(attr, DbusSignalAsync): + meta.dbus_member_to_python_attr[attr.signal_name] = attr_name + meta.python_attr_to_dbus_member[attr_name] = attr.signal_name + else: + raise TypeError(f"Unknown D-Bus element: {attr!r}") + + def __new__(cls, name: str, + bases: tuple[type, ...], + namespace: dict[str, Any], + interface_name: Optional[str] = None, + serving_enabled: bool = True, + ) -> DbusInterfaceMetaAsync: + + if interface_name in DBUS_INTERFACE_NAME_TO_CLASS: + raise ValueError( + f"D-Bus interface of the name {interface_name!r} was " + "already created." + ) + + all_mro_bases: set[type[Any]] = set( + chain.from_iterable(c.__mro__ for c in bases) + ) + reserved_dbus_map = cls._map_mro_dbus_elements( + name, all_mro_bases, + ) + cls._check_collisions(name, namespace, reserved_dbus_map) - namespace['_dbus_served_interfaces_names'] = \ - dbus_served_interfaces_names - namespace['_dbus_to_python_name_map'] = dbus_to_python_name_map - namespace['_dbus_interface_name'] = interface_name - namespace['_dbus_serving_enabled'] = serving_enabled - namespace['_dbus_declared_members'] = dbus_declared_members new_cls = super().__new__( cls, name, bases, namespace, interface_name, serving_enabled, ) - return cast(DbusInterfaceMetaAsync, new_cls) + if interface_name is not None: + dbus_class_meta = DbusClassMeta(interface_name, serving_enabled) + DBUS_CLASS_TO_META[new_cls] = dbus_class_meta + DBUS_INTERFACE_NAME_TO_CLASS[interface_name] = new_cls + + for attr_name, attr in namespace.items(): + cls._map_dbus_elements( + attr_name, + attr, + dbus_class_meta, + interface_name, + ) + + return new_cls class DbusInterfaceBaseAsync(metaclass=DbusInterfaceMetaAsync): - _dbus_interface_name: Optional[str] - _dbus_serving_enabled: bool - _dbus_to_python_name_map: Dict[str, str] - _dbus_served_interfaces_names: Set[str] - _dbus_declared_members: Dict[str, DbusSomethingAsync] def __init__(self) -> None: - self._activated_interfaces: List[SdBusInterface] = [] - self._is_binded: bool = False - self._remote_service_name: Optional[str] = None - self._remote_object_path: Optional[str] = None - self._attached_bus: Optional[SdBus] = None - self._serving_object_path: Optional[str] = None - self._local_signal_queues: \ - Dict[DbusSignalAsync[Any], List[weak_ref[Queue[Any]]]] = {} + self._dbus: Union[ + DbusRemoteObjectMeta, DbusLocalObjectMeta] = DbusLocalObjectMeta() + + @classmethod + def _dbus_iter_interfaces_meta( + cls, + ) -> Iterator[tuple[str, DbusClassMeta]]: + + for base in reversed(cls.__mro__): + meta = DBUS_CLASS_TO_META.get(base) + if meta is None: + continue + + yield meta.interface_name, meta async def start_serving(self, object_path: str, @@ -181,52 +311,42 @@ async def start_serving(self, DeprecationWarning) self.export_to_dbus(object_path, bus) + def _dbus_on_no_members_exported(self) -> None: + raise ValueError("No D-Bus interfaces were exported") + def export_to_dbus( self, object_path: str, bus: Optional[SdBus] = None, - ) -> None: + ) -> DbusExportHandle: + local_object_meta = self._dbus + if isinstance(local_object_meta, DbusRemoteObjectMeta): + raise RuntimeError("Cannot export D-Bus proxies.") - if bus is None: - bus = get_default_bus() # TODO: Being able to serve multiple buses and object - self._attached_bus = bus - self._serving_object_path = object_path - # TODO: can be optimized with a single loop - interface_map: Dict[str, List[DbusBindedAsync]] = {} - - for key, value in getmembers(self): - assert not isinstance(value, DbusSomethingAsync) - - if isinstance(value, DbusMethodAsyncBinded): - interface_name = value.dbus_method.interface_name - if not value.dbus_method.serving_enabled: - continue - elif isinstance(value, DbusPropertyAsyncBinded): - interface_name = value.dbus_property.interface_name - if not value.dbus_property.serving_enabled: - continue - elif isinstance(value, DbusSignalBinded): - interface_name = value.dbus_signal.interface_name - if not value.dbus_signal.serving_enabled: - continue - else: - continue + if local_object_meta.attached_bus is not None: + raise RuntimeError( + "Object already exported. " + "This limitation should be fixed in future version." + ) - assert interface_name is not None + if bus is None: + bus = get_default_bus() - try: - interface_member_list = interface_map[interface_name] - except KeyError: - interface_member_list = [] - interface_map[interface_name] = interface_member_list + local_object_meta.attached_bus = bus + local_object_meta.serving_object_path = object_path - interface_member_list.append(value) + for interface_name, meta in self._dbus_iter_interfaces_meta(): + if not meta.serving_enabled: + continue - for interface_name, member_list in interface_map.items(): new_interface = SdBusInterface() - for dbus_something in member_list: - if isinstance(dbus_something, DbusMethodAsyncBinded): + + for python_attr, dbus_member in ( + meta.python_attr_to_dbus_member.items() + ): + dbus_something = getattr(self, python_attr) + if isinstance(dbus_something, DbusLocalMethodAsync): new_interface.add_method( dbus_something.dbus_method.method_name, dbus_something.dbus_method.input_signature, @@ -234,24 +354,29 @@ def export_to_dbus( dbus_something.dbus_method.result_signature, dbus_something.dbus_method.result_args_names, dbus_something.dbus_method.flags, - dbus_something._call_from_dbus, + dbus_something._dbus_reply_call, ) - elif isinstance(dbus_something, DbusPropertyAsyncBinded): - getter = dbus_something._reply_get_sync - - setter = (dbus_something._reply_set_sync - if dbus_something.dbus_property.property_setter - is not None - else None) + elif isinstance(dbus_something, DbusLocalPropertyAsync): + getter = dbus_something._dbus_reply_get + dbus_property = dbus_something.dbus_property + + if ( + dbus_property.property_setter is not None + and + dbus_property.property_setter_is_public + ): + setter = dbus_something._dbus_reply_set + else: + setter = None new_interface.add_property( - dbus_something.dbus_property.property_name, - dbus_something.dbus_property.property_signature, + dbus_property.property_name, + dbus_property.property_signature, getter, setter, - dbus_something.dbus_property.flags, + dbus_property.flags, ) - elif isinstance(dbus_something, DbusSignalBinded): + elif isinstance(dbus_something, DbusLocalSignalAsync): new_interface.add_signal( dbus_something.dbus_signal.signal_name, dbus_something.dbus_signal.signal_signature, @@ -259,11 +384,17 @@ def export_to_dbus( dbus_something.dbus_signal.flags, ) else: - raise TypeError + raise TypeError( + f"Expected D-Bus element, got: {dbus_something!r}" + ) + + bus.add_interface(new_interface, object_path, interface_name) + local_object_meta.activated_interfaces.append(new_interface) - bus.add_interface(new_interface, object_path, - interface_name) - self._activated_interfaces.append(new_interface) + if not local_object_meta.activated_interfaces: + self._dbus_on_no_members_exported() + + return DbusExportHandle(local_object_meta) def _connect( self, @@ -284,39 +415,75 @@ def _proxify( bus: Optional[SdBus] = None, ) -> None: - self._is_binded = True - self._attached_bus = bus if bus is not None else get_default_bus() - self._remote_service_name = service_name - self._remote_object_path = object_path + self._dbus = DbusRemoteObjectMeta( + service_name, + object_path, + bus, + ) @classmethod def new_connect( - cls: Type[T_input], + cls: type[Self], service_name: str, object_path: str, bus: Optional[SdBus] = None, - ) -> T_input: + ) -> Self: warn( ("new_connect is deprecated in favor of equivalent new_proxy." "Will be removed in version 1.0.0"), DeprecationWarning, ) new_object = cls.__new__(cls) - assert isinstance(new_object, DbusInterfaceBaseAsync) new_object._proxify(service_name, object_path, bus) - assert isinstance(new_object, cls) return new_object @classmethod def new_proxy( - cls: Type[T_input], + cls: type[Self], service_name: str, object_path: str, bus: Optional[SdBus] = None, - ) -> T_input: + ) -> Self: new_object = cls.__new__(cls) - assert isinstance(new_object, DbusInterfaceBaseAsync) new_object._proxify(service_name, object_path, bus) - assert isinstance(new_object, cls) return new_object + + +class DbusExportHandle: + def __init__(self, local_meta: DbusLocalObjectMeta): + self._tasks = local_meta.tasks + self._dbus_slots: list[SdBusSlot] = [] + self._dbus_interfaces = local_meta.activated_interfaces + + async def __aenter__(self) -> DbusExportHandle: + return self + + def __enter__(self) -> DbusExportHandle: + return self + + def __exit__( + self, + exc_type: Any, + exc_value: Any, + traceback: Any, + ) -> None: + self.stop() + + async def __aexit__( + self, + exc_type: Any, + exc_value: Any, + traceback: Any, + ) -> None: + self.stop() + + def stop(self) -> None: + for task in self._tasks: + task.cancel("D-Bus export stopped") + + for interface in self._dbus_interfaces: + interface._stop_export() + + for slot in self._dbus_slots: + slot.close() diff --git a/src/sdbus/dbus_proxy_async_interfaces.py b/src/sdbus/dbus_proxy_async_interfaces.py index 1b1c809..303aaff 100644 --- a/src/sdbus/dbus_proxy_async_interfaces.py +++ b/src/sdbus/dbus_proxy_async_interfaces.py @@ -1,6 +1,6 @@ # SPDX-License-Identifier: LGPL-2.1-or-later -# Copyright (C) 2020-2022 igo95862 +# Copyright (C) 2020-2024 igo95862 # This file is part of python-sdbus @@ -19,15 +19,23 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA from __future__ import annotations -from inspect import getmembers -from typing import Any, Dict, List, Literal, Optional, Tuple +from typing import TYPE_CHECKING -from .dbus_common_funcs import get_default_bus +from .dbus_common_funcs import _parse_properties_vardict from .dbus_proxy_async_interface_base import DbusInterfaceBaseAsync from .dbus_proxy_async_method import dbus_method_async -from .dbus_proxy_async_property import DbusPropertyAsyncBinded from .dbus_proxy_async_signal import dbus_signal_async -from .sd_bus_internals import DbusPropertyEmitsChangeFlag, SdBus, SdBusSlot + +if TYPE_CHECKING: + from typing import Any, Literal + + DBUS_PROPERTIES_CHANGED_TYPING = ( + tuple[ + str, + dict[str, tuple[str, Any]], + list[str], + ] + ) class DbusPeerInterfaceAsync( @@ -56,130 +64,48 @@ async def dbus_introspect(self) -> str: raise NotImplementedError -DBUS_PROPERTIES_CHANGED_TYPING = Tuple[str, - Dict[str, Tuple[str, Any]], - List[str]] - - class DbusPropertiesInterfaceAsync( DbusInterfaceBaseAsync, interface_name='org.freedesktop.DBus.Properties', serving_enabled=False, ): - def __init__(self) -> None: - super().__init__() - - for key, value in getmembers(self): - if isinstance(value, DbusPropertyAsyncBinded): - if not value.dbus_property.flags & DbusPropertyEmitsChangeFlag: - continue @dbus_signal_async('sa{sv}as') def properties_changed(self) -> DBUS_PROPERTIES_CHANGED_TYPING: - ... + raise NotImplementedError @dbus_method_async('s', 'a{sv}', method_name='GetAll') async def _properties_get_all( - self, interface_name: str) -> Dict[str, Tuple[str, Any]]: + self, interface_name: str) -> dict[str, tuple[str, Any]]: raise NotImplementedError async def properties_get_all_dict( self, on_unknown_member: Literal['error', 'ignore', 'reuse'] = 'error', - ) -> Dict[str, Any]: + ) -> dict[str, Any]: + + properties: dict[str, Any] = {} - properties: Dict[str, Any] = {} + for interface_name, meta in self._dbus_iter_interfaces_meta(): + if not meta.serving_enabled: + continue - for interface_name in self._dbus_served_interfaces_names: dbus_properties_data = await self._properties_get_all( interface_name) - for member_name, variant in dbus_properties_data.items(): - try: - python_name = self._dbus_to_python_name_map[member_name] - except KeyError: - if on_unknown_member == 'error': - raise - elif on_unknown_member == 'ignore': - continue - elif on_unknown_member == 'reuse': - python_name = member_name - else: - raise ValueError - - properties[python_name] = variant[1] + properties.update( + _parse_properties_vardict( + meta.dbus_member_to_python_attr, + dbus_properties_data, + on_unknown_member, + ) + ) return properties class DbusInterfaceCommonAsync( - DbusPeerInterfaceAsync, DbusPropertiesInterfaceAsync, - DbusIntrospectableAsync): + DbusPropertiesInterfaceAsync, + DbusIntrospectableAsync, + DbusPeerInterfaceAsync): ... - - -class DbusObjectManagerInterfaceAsync( - DbusInterfaceCommonAsync, - interface_name='org.freedesktop.DBus.ObjectManager', - serving_enabled=False, -): - def __init__(self) -> None: - super().__init__() - self._object_manager_slot: Optional[SdBusSlot] = None - self._managed_object_to_path: Dict[DbusInterfaceBaseAsync, str] = {} - - @dbus_method_async(result_signature='a{oa{sa{sv}}}') - async def get_managed_objects( - self) -> Dict[str, Dict[str, Dict[str, Any]]]: - raise NotImplementedError - - @dbus_signal_async('oa{sa{sv}}') - def interfaces_added(self) -> Tuple[str, Dict[str, Dict[str, Any]]]: - raise NotImplementedError - - @dbus_signal_async('oao') - def interfaces_removed(self) -> Tuple[str, List[str]]: - raise NotImplementedError - - def export_to_dbus( - self, - object_path: str, - bus: Optional[SdBus] = None, - ) -> None: - if bus is None: - bus = get_default_bus() - - super().export_to_dbus( - object_path, - bus, - ) - slot = bus.add_object_manager(object_path) - self._object_manager_slot = slot - - def export_with_manager( - self, - object_path: str, - object_to_export: DbusInterfaceBaseAsync, - bus: Optional[SdBus] = None, - ) -> None: - if self._object_manager_slot is None: - raise RuntimeError('ObjectManager not intitialized') - - if bus is None: - bus = get_default_bus() - - object_to_export.export_to_dbus( - object_path, - bus, - ) - bus.emit_object_added(object_path) - self._managed_object_to_path[object_to_export] = object_path - - def remove_managed_object( - self, - managed_object: DbusInterfaceBaseAsync) -> None: - if self._attached_bus is None: - raise RuntimeError('Object manager not exported') - - removed_path = self._managed_object_to_path.pop(managed_object) - self._attached_bus.emit_object_removed(removed_path) diff --git a/src/sdbus/dbus_proxy_async_method.py b/src/sdbus/dbus_proxy_async_method.py index d42a405..3ac3c2a 100644 --- a/src/sdbus/dbus_proxy_async_method.py +++ b/src/sdbus/dbus_proxy_async_method.py @@ -1,6 +1,6 @@ # SPDX-License-Identifier: LGPL-2.1-or-later -# Copyright (C) 2020-2022 igo95862 +# Copyright (C) 2020-2023 igo95862 # This file is part of python-sdbus @@ -19,29 +19,34 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA from __future__ import annotations +from asyncio import get_running_loop from contextvars import ContextVar, copy_context from inspect import iscoroutinefunction from types import FunctionType -from typing import ( - TYPE_CHECKING, - Any, - Callable, - Optional, - Sequence, - Type, - TypeVar, - cast, -) +from typing import TYPE_CHECKING, cast, overload from weakref import ref as weak_ref from .dbus_common_elements import ( - DbusBindedAsync, + DbusBoundAsync, + DbusLocalObjectMeta, + DbusMemberAsync, DbusMethodCommon, - DbusOverload, - DbusSomethingAsync, + DbusMethodOverride, + DbusRemoteObjectMeta, ) from .dbus_exceptions import DbusFailedError -from .sd_bus_internals import DbusNoReplyFlag, SdBusMessage +from .sd_bus_internals import EXCEPTION_TO_DBUS_ERROR, DbusNoReplyFlag + +if TYPE_CHECKING: + from collections.abc import Callable, Sequence + from typing import Any, Optional, TypeVar, Union + + from .dbus_proxy_async_interface_base import DbusInterfaceBaseAsync + from .sd_bus_internals import SdBusMessage + + T = TypeVar('T') +else: + T = None CURRENT_MESSAGE: ContextVar[SdBusMessage] = ContextVar('CURRENT_MESSAGE') @@ -50,138 +55,175 @@ def get_current_message() -> SdBusMessage: return CURRENT_MESSAGE.get() -T_input = TypeVar('T_input') -T = TypeVar('T') - +class DbusMethodAsync(DbusMethodCommon, DbusMemberAsync): + + @overload + def __get__( + self, + obj: None, + obj_class: type[DbusInterfaceBaseAsync], + ) -> DbusMethodAsync: + ... + + @overload + def __get__( + self, + obj: DbusInterfaceBaseAsync, + obj_class: type[DbusInterfaceBaseAsync], + ) -> Callable[..., Any]: + ... + + def __get__( + self, + obj: Optional[DbusInterfaceBaseAsync], + obj_class: Optional[type[DbusInterfaceBaseAsync]] = None, + ) -> Union[Callable[..., Any], DbusMethodAsync]: + if obj is not None: + dbus_meta = obj._dbus + if isinstance(dbus_meta, DbusRemoteObjectMeta): + return DbusProxyMethodAsync(self, dbus_meta) + else: + return DbusLocalMethodAsync(self, obj) + else: + return self -if TYPE_CHECKING: - from .dbus_proxy_async_interface_base import DbusInterfaceBaseAsync +class DbusBoundMethodAsyncBase(DbusBoundAsync): -class DbusMethodAsync(DbusMethodCommon, DbusSomethingAsync): - def __get__(self, - obj: DbusInterfaceBaseAsync, - obj_class: Optional[Type[DbusInterfaceBaseAsync]] = None, - ) -> Callable[..., Any]: - return DbusMethodAsyncBinded(self, obj) + def __call__(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError -class DbusMethodAsyncBinded(DbusBindedAsync): - def __init__(self, - dbus_method: DbusMethodAsync, - interface: DbusInterfaceBaseAsync): +class DbusProxyMethodAsync(DbusBoundMethodAsyncBase): + def __init__( + self, + dbus_method: DbusMethodAsync, + proxy_meta: DbusRemoteObjectMeta, + ): self.dbus_method = dbus_method - self.interface_ref = ( - weak_ref(interface) - if interface is not None - else None - ) + self.proxy_meta = proxy_meta self.__doc__ = dbus_method.__doc__ - async def _call_dbus_async(self, *args: Any) -> Any: - assert self.interface_ref is not None - interface = self.interface_ref() - assert interface is not None - - assert interface._attached_bus is not None - assert interface._remote_service_name is not None - assert interface._remote_object_path is not None - assert self.dbus_method.interface_name is not None - new_call_message = interface._attached_bus. \ - new_method_call_message( - interface._remote_service_name, - interface._remote_object_path, - self.dbus_method.interface_name, - self.dbus_method.method_name, - ) + async def _dbus_async_call(self, call_message: SdBusMessage) -> Any: + bus = self.proxy_meta.attached_bus + reply_message = await bus.call_async(call_message) + return reply_message.get_contents() + + @staticmethod + async def _no_reply() -> None: + return None + + def __call__(self, *args: Any, **kwargs: Any) -> Any: + bus = self.proxy_meta.attached_bus + dbus_method = self.dbus_method + + new_call_message = bus.new_method_call_message( + self.proxy_meta.service_name, + self.proxy_meta.object_path, + dbus_method.interface_name, + dbus_method.method_name, + ) + + if len(args) == dbus_method.num_of_args: + assert not kwargs, ( + "Passed more arguments than method supports" + f"Extra args: {kwargs}") + rebuilt_args: Sequence[Any] = args + else: + rebuilt_args = dbus_method._rebuild_args( + dbus_method.original_method, + *args, + **kwargs) - if args: + if rebuilt_args: new_call_message.append_data( - self.dbus_method.input_signature, *args) + dbus_method.input_signature, *rebuilt_args) - if self.dbus_method.flags & DbusNoReplyFlag: + if dbus_method.flags & DbusNoReplyFlag: new_call_message.expect_reply = False new_call_message.send() - return + return self._no_reply() - reply_message = await interface._attached_bus.call_async( - new_call_message) - return reply_message.get_contents() + return self._dbus_async_call(new_call_message) - def __call__(self, *args: Any, **kwargs: Any) -> Any: - assert self.interface_ref is not None - interface = self.interface_ref() - assert interface is not None - if interface._is_binded: +class DbusLocalMethodAsync(DbusBoundMethodAsyncBase): + def __init__( + self, + dbus_method: DbusMethodAsync, + local_object: DbusInterfaceBaseAsync, + ): + self.dbus_method = dbus_method + self.local_object_ref = weak_ref(local_object) + + self.__doc__ = dbus_method.__doc__ - if len(args) == self.dbus_method.num_of_args: - assert not kwargs, ( - "Passed more arguments than method supports" - f"Extra args: {kwargs}") - rebuilt_args: Sequence[Any] = args - else: - rebuilt_args = self.dbus_method._rebuild_args( - self.dbus_method.original_method, - *args, - **kwargs) + def __call__(self, *args: Any, **kwargs: Any) -> Any: + local_object = self.local_object_ref() + if local_object is None: + raise RuntimeError("Local object no longer exists!") - return self._call_dbus_async(*rebuilt_args) - else: - return self.dbus_method.original_method( - interface, *args, **kwargs) + return self.dbus_method.original_method(local_object, *args, **kwargs) - async def _call_method_from_dbus( - self, - request_message: SdBusMessage, - interface: DbusInterfaceBaseAsync) -> Any: - request_data = request_message.get_contents() + async def _dbus_reply_call_method( + self, + request_message: SdBusMessage, + local_object: DbusInterfaceBaseAsync, + ) -> Any: local_method = self.dbus_method.original_method.__get__( - interface, None) + local_object, None) CURRENT_MESSAGE.set(request_message) - if isinstance(request_data, tuple): - return await local_method(*request_data) - elif request_data is None: - return await local_method() - else: - return await local_method(request_data) - - async def _call_from_dbus( - self, - request_message: SdBusMessage) -> None: - assert self.interface_ref is not None - interface = self.interface_ref() - assert interface is not None - + return await local_method(*request_message.parse_to_tuple()) + + def _dbus_reply_call( + self, + request_message: SdBusMessage + ) -> None: + local_object = self.local_object_ref() + if local_object is None: + raise RuntimeError("Local object no longer exists!") + local_meta = local_object._dbus + if not isinstance(local_meta, DbusLocalObjectMeta): + raise RuntimeError("D-Bus object is a remote proxy!") + + loop = get_running_loop() + reply_task = loop.create_task( + self._dbus_reply_call_async(local_object, request_message) + ) + tasks_set = local_meta.tasks + tasks_set.add(reply_task) + reply_task.add_done_callback(tasks_set.discard) + + async def _dbus_reply_call_async( + self, + local_object: DbusInterfaceBaseAsync, + request_message: SdBusMessage + ) -> None: call_context = copy_context() - try: reply_data = await call_context.run( - self._call_method_from_dbus, + self._dbus_reply_call_method, request_message, - interface, + local_object, ) - except DbusFailedError as e: + except Exception as e: if not request_message.expect_reply: return + dbus_error = EXCEPTION_TO_DBUS_ERROR.get(type(e)) + if dbus_error is None: + dbus_error = DbusFailedError.dbus_error_name + error_message = request_message.create_error_reply( - e.dbus_error_name, + dbus_error, str(e.args[0]) if e.args else "", ) error_message.send() return - except Exception: - error_message = request_message.create_error_reply( - DbusFailedError.dbus_error_name, - "", - ) - error_message.send() - return if not request_message.expect_reply: return @@ -209,17 +251,17 @@ def dbus_method_async( input_signature: str = "", result_signature: str = "", flags: int = 0, - result_args_names: Sequence[str] = (), - input_args_names: Sequence[str] = (), + result_args_names: Optional[Sequence[str]] = None, + input_args_names: Optional[Sequence[str]] = None, method_name: Optional[str] = None, -) -> Callable[[T_input], T_input]: +) -> Callable[[T], T]: assert not isinstance(input_signature, FunctionType), ( "Passed function to decorator directly. " "Did you forget () round brackets?" ) - def dbus_method_decorator(original_method: T_input) -> T_input: + def dbus_method_decorator(original_method: T) -> T: assert isinstance(original_method, FunctionType) assert iscoroutinefunction(original_method), ( "Expected coroutine function. ", @@ -235,7 +277,7 @@ def dbus_method_decorator(original_method: T_input) -> T_input: flags=flags, ) - return cast(T_input, new_wrapper) + return cast(T, new_wrapper) return dbus_method_decorator @@ -244,6 +286,6 @@ def dbus_method_async_override() -> Callable[[T], T]: def new_decorator( new_function: T) -> T: - return cast(T, DbusOverload(new_function)) + return cast(T, DbusMethodOverride(new_function)) return new_decorator diff --git a/src/sdbus/dbus_proxy_async_object_manager.py b/src/sdbus/dbus_proxy_async_object_manager.py new file mode 100644 index 0000000..06d636e --- /dev/null +++ b/src/sdbus/dbus_proxy_async_object_manager.py @@ -0,0 +1,134 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later + +# Copyright (C) 2020-2024 igo95862 + +# This file is part of python-sdbus + +# 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 __future__ import annotations + +from functools import partial +from typing import TYPE_CHECKING + +from .dbus_common_elements import DbusLocalObjectMeta +from .dbus_proxy_async_interface_base import ( + DbusExportHandle, + DbusInterfaceBaseAsync, +) +from .dbus_proxy_async_interfaces import DbusInterfaceCommonAsync +from .dbus_proxy_async_method import dbus_method_async +from .dbus_proxy_async_signal import dbus_signal_async +from .default_bus import get_default_bus + +if TYPE_CHECKING: + from collections.abc import Callable + from typing import Any, Optional + + from .sd_bus_internals import SdBus, SdBusSlot + + +class DbusObjectManagerExportHandle(DbusExportHandle): + def __init__( + self, + local_meta: DbusLocalObjectMeta, + remove_object_call: Callable[[], None], + ): + super().__init__(local_meta) + self.remove_object_call = remove_object_call + + def stop(self) -> None: + self.remove_object_call() + super().stop() + + +class DbusObjectManagerInterfaceAsync( + DbusInterfaceCommonAsync, + interface_name='org.freedesktop.DBus.ObjectManager', + serving_enabled=False, +): + def __init__(self) -> None: + super().__init__() + self._object_manager_slot: Optional[SdBusSlot] = None + self._managed_object_to_path: dict[DbusInterfaceBaseAsync, str] = {} + + @dbus_method_async(result_signature='a{oa{sa{sv}}}') + async def get_managed_objects( + self) -> dict[str, dict[str, dict[str, Any]]]: + raise NotImplementedError + + @dbus_signal_async('oa{sa{sv}}') + def interfaces_added(self) -> tuple[str, dict[str, dict[str, Any]]]: + raise NotImplementedError + + @dbus_signal_async('oao') + def interfaces_removed(self) -> tuple[str, list[str]]: + raise NotImplementedError + + def _dbus_on_no_members_exported(self) -> None: + ... # Object manager is allowed to be exported empty + + def export_to_dbus( + self, + object_path: str, + bus: Optional[SdBus] = None, + ) -> DbusExportHandle: + if bus is None: + bus = get_default_bus() + + export_handle = super().export_to_dbus( + object_path, + bus, + ) + slot = bus.add_object_manager(object_path) + self._object_manager_slot = slot + export_handle._dbus_slots.append(slot) + return export_handle + + def export_with_manager( + self, + object_path: str, + object_to_export: DbusInterfaceBaseAsync, + bus: Optional[SdBus] = None, + ) -> DbusObjectManagerExportHandle: + if self._object_manager_slot is None: + raise RuntimeError('ObjectManager not intitialized') + + if bus is None: + bus = get_default_bus() + + object_to_export.export_to_dbus( + object_path, + bus, + ) + meta = object_to_export._dbus + if not isinstance(meta, DbusLocalObjectMeta): + raise TypeError + handle = DbusObjectManagerExportHandle( + meta, + partial(self.remove_managed_object, object_to_export), + ) + bus.emit_object_added(object_path) + self._managed_object_to_path[object_to_export] = object_path + + return handle + + def remove_managed_object( + self, + managed_object: DbusInterfaceBaseAsync) -> None: + if self._dbus.attached_bus is None: + raise RuntimeError('Object manager not exported') + + removed_path = self._managed_object_to_path.pop(managed_object) + self._dbus.attached_bus.emit_object_removed(removed_path) diff --git a/src/sdbus/dbus_proxy_async_property.py b/src/sdbus/dbus_proxy_async_property.py index 200a859..bf63f04 100644 --- a/src/sdbus/dbus_proxy_async_property.py +++ b/src/sdbus/dbus_proxy_async_property.py @@ -1,6 +1,6 @@ # SPDX-License-Identifier: LGPL-2.1-or-later -# Copyright (C) 2020-2022 igo95862 +# Copyright (C) 2020-2023 igo95862 # This file is part of python-sdbus @@ -19,37 +19,32 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA from __future__ import annotations +from collections.abc import Awaitable from inspect import iscoroutinefunction from types import FunctionType -from typing import ( - TYPE_CHECKING, - Any, - Callable, - Generator, - Generic, - Optional, - Type, - TypeVar, - cast, -) +from typing import TYPE_CHECKING, Generic, TypeVar, cast, overload from weakref import ref as weak_ref from .dbus_common_elements import ( - DbusBindedAsync, - DbusOverload, + DbusBoundAsync, + DbusMemberAsync, DbusPropertyCommon, - DbusSomethingAsync, + DbusPropertyOverride, + DbusRemoteObjectMeta, ) -from .sd_bus_internals import SdBusMessage - -T = TypeVar('T') - if TYPE_CHECKING: + from collections.abc import Callable, Generator + from typing import Any, Optional, Union + from .dbus_proxy_async_interface_base import DbusInterfaceBaseAsync + from .sd_bus_internals import SdBusMessage -class DbusPropertyAsync(DbusSomethingAsync, DbusPropertyCommon, Generic[T]): +T = TypeVar('T') + + +class DbusPropertyAsync(DbusMemberAsync, DbusPropertyCommon, Generic[T]): def __init__( self, property_name: Optional[str], @@ -74,90 +69,193 @@ def __init__( self.property_setter: Optional[ Callable[[DbusInterfaceBaseAsync, T], None]] = property_setter + self.property_setter_is_public: bool = True self.__doc__ = property_getter.__doc__ - def __get__(self, - obj: DbusInterfaceBaseAsync, - obj_class: Optional[Type[DbusInterfaceBaseAsync]] = None, - ) -> DbusPropertyAsyncBinded: - return DbusPropertyAsyncBinded(self, obj) + @overload + def __get__( + self, + obj: None, + obj_class: type[DbusInterfaceBaseAsync], + ) -> DbusPropertyAsync[T]: + ... + + @overload + def __get__( + self, + obj: DbusInterfaceBaseAsync, + obj_class: type[DbusInterfaceBaseAsync], + ) -> DbusBoundPropertyAsyncBase[T]: + ... + + def __get__( + self, + obj: Optional[DbusInterfaceBaseAsync], + obj_class: Optional[type[DbusInterfaceBaseAsync]] = None, + ) -> Union[DbusBoundPropertyAsyncBase[T], DbusPropertyAsync[T]]: + if obj is not None: + dbus_meta = obj._dbus + if isinstance(dbus_meta, DbusRemoteObjectMeta): + return DbusProxyPropertyAsync(self, dbus_meta) + else: + return DbusLocalPropertyAsync(self, obj) + else: + return self def setter(self, new_set_function: Callable[ [Any, T], - None] + None], ) -> None: + assert self.property_setter is None, "Setter already defined" assert not iscoroutinefunction(new_set_function), ( "Property setter can't be coroutine", ) self.property_setter = new_set_function - -class DbusPropertyAsyncBinded(DbusBindedAsync): - def __init__(self, - dbus_property: DbusPropertyAsync[T], - interface: DbusInterfaceBaseAsync): - self.dbus_property = dbus_property - self.interface_ref = ( - weak_ref(interface) - if interface is not None - else None + def setter_private( + self, + new_set_function: Callable[ + [Any, T], + None], + ) -> None: + assert self.property_setter is None, "Setter already defined" + assert not iscoroutinefunction(new_set_function), ( + "Property setter can't be coroutine", ) + self.property_setter = new_set_function + self.property_setter_is_public = False - self.__doc__ = dbus_property.__doc__ +class DbusBoundPropertyAsyncBase(DbusBoundAsync, Awaitable[T]): def __await__(self) -> Generator[Any, None, T]: return self.get_async().__await__() async def get_async(self) -> T: - assert self.interface_ref is not None - interface = self.interface_ref() - assert interface is not None - - if not interface._is_binded: - return self.dbus_property.property_getter( - interface) - - assert interface._attached_bus is not None - assert interface._remote_service_name is not None - assert interface._remote_object_path is not None - assert self.dbus_property.property_name is not None - assert self.dbus_property.interface_name is not None - new_call_message = interface._attached_bus. \ - new_property_get_message( - interface._remote_service_name, - interface._remote_object_path, + raise NotImplementedError + + async def set_async(self, complete_object: T) -> None: + raise NotImplementedError + + +class DbusProxyPropertyAsync(DbusBoundPropertyAsyncBase[T]): + def __init__( + self, + dbus_property: DbusPropertyAsync[T], + proxy_meta: DbusRemoteObjectMeta, + ): + self.dbus_property = dbus_property + self.proxy_meta = proxy_meta + + self.__doc__ = dbus_property.__doc__ + + async def get_async(self) -> T: + bus = self.proxy_meta.attached_bus + new_get_message = ( + bus.new_property_get_message( + self.proxy_meta.service_name, + self.proxy_meta.object_path, self.dbus_property.interface_name, self.dbus_property.property_name, ) - - reply_message = await interface._attached_bus. \ - call_async(new_call_message) + ) + reply_message = await bus.call_async(new_get_message) # Get method returns variant but we only need contents of variant return cast(T, reply_message.get_contents()[1]) - def _reply_get_sync(self, message: SdBusMessage) -> None: - assert self.interface_ref is not None - interface = self.interface_ref() - assert interface is not None + async def set_async(self, complete_object: T) -> None: + bus = self.proxy_meta.attached_bus + new_set_message = ( + bus.new_property_set_message( + self.proxy_meta.service_name, + self.proxy_meta.object_path, + self.dbus_property.interface_name, + self.dbus_property.property_name, + ) + ) + new_set_message.append_data( + 'v', + (self.dbus_property.property_signature, complete_object), + ) + await bus.call_async(new_set_message) + + +class DbusLocalPropertyAsync(DbusBoundPropertyAsyncBase[T]): + def __init__( + self, + dbus_property: DbusPropertyAsync[T], + local_object: DbusInterfaceBaseAsync, + ): + self.dbus_property = dbus_property + self.local_object_ref = weak_ref(local_object) + + self.__doc__ = dbus_property.__doc__ + + async def get_async(self) -> T: + local_object = self.local_object_ref() + if local_object is None: + raise RuntimeError("Local object no longer exists!") + + return self.dbus_property.property_getter(local_object) + + async def set_async(self, complete_object: T) -> None: + if self.dbus_property.property_setter is None: + raise RuntimeError("Property has no setter") + + local_object = self.local_object_ref() + if local_object is None: + raise RuntimeError("Local object no longer exists!") + + self.dbus_property.property_setter( + local_object, + complete_object, + ) + + try: + properties_changed = getattr( + local_object, + "properties_changed", + ) + except AttributeError: + ... + else: + properties_changed.emit( + ( + self.dbus_property.interface_name, + { + self.dbus_property.property_name: ( + self.dbus_property.property_signature, + complete_object, + ), + }, + [] + ) + ) + + def _dbus_reply_get(self, message: SdBusMessage) -> None: + local_object = self.local_object_ref() + if local_object is None: + raise RuntimeError("Local object no longer exists!") - reply_data: Any = self.dbus_property.property_getter(interface) + reply_data: Any = self.dbus_property.property_getter(local_object) message.append_data(self.dbus_property.property_signature, reply_data) - def _reply_set_sync(self, message: SdBusMessage) -> None: - assert self.interface_ref is not None - interface = self.interface_ref() - assert interface is not None + def _dbus_reply_set(self, message: SdBusMessage) -> None: + local_object = self.local_object_ref() + if local_object is None: + raise RuntimeError("Local object no longer exists!") assert self.dbus_property.property_setter is not None data_to_set_to: Any = message.get_contents() - self.dbus_property.property_setter(interface, data_to_set_to) + self.dbus_property.property_setter(local_object, data_to_set_to) - assert self.dbus_property.interface_name is not None try: - properties_changed = getattr(interface, 'properties_changed') + properties_changed = getattr( + local_object, + "properties_changed", + ) except AttributeError: ... else: @@ -174,38 +272,6 @@ def _reply_set_sync(self, message: SdBusMessage) -> None: ) ) - async def set_async(self, complete_object: T) -> None: - assert self.interface_ref is not None - interface = self.interface_ref() - assert interface is not None - - if not interface._is_binded: - if self.dbus_property.property_setter is None: - raise ValueError('Property has no setter') - - self.dbus_property.property_setter( - interface, complete_object) - - return - - assert interface._attached_bus is not None - assert interface._remote_service_name is not None - assert interface._remote_object_path is not None - assert self.dbus_property.property_name is not None - assert self.dbus_property.interface_name is not None - new_call_message = interface._attached_bus. \ - new_property_set_message( - interface._remote_service_name, - interface._remote_object_path, - self.dbus_property.interface_name, - self.dbus_property.property_name, - ) - - new_call_message.append_data( - 'v', (self.dbus_property.property_signature, complete_object)) - - await interface._attached_bus.call_async(new_call_message) - def dbus_property_async( property_signature: str = "", @@ -225,7 +291,7 @@ def property_decorator( ) -> DbusPropertyAsync[T]: assert not iscoroutinefunction(function), ( - "Property setter can't be coroutine", + "Property getter can't be coroutine", ) new_wrapper: DbusPropertyAsync[T] = DbusPropertyAsync( @@ -247,6 +313,6 @@ def dbus_property_async_override() -> Callable[ def new_decorator( new_property: Callable[[Any], T]) -> DbusPropertyAsync[T]: - return cast(DbusPropertyAsync[T], DbusOverload(new_property)) + return cast(DbusPropertyAsync[T], DbusPropertyOverride(new_property)) return new_decorator diff --git a/src/sdbus/dbus_proxy_async_signal.py b/src/sdbus/dbus_proxy_async_signal.py index 7d834cc..e99df9b 100644 --- a/src/sdbus/dbus_proxy_async_signal.py +++ b/src/sdbus/dbus_proxy_async_signal.py @@ -1,6 +1,6 @@ # SPDX-License-Identifier: LGPL-2.1-or-later -# Copyright (C) 2020-2022 igo95862 +# Copyright (C) 2020-2023 igo95862 # This file is part of python-sdbus @@ -20,130 +20,166 @@ from __future__ import annotations from asyncio import Queue +from collections.abc import AsyncIterable, AsyncIterator +from contextlib import closing from types import FunctionType -from typing import ( - TYPE_CHECKING, - Any, - AsyncGenerator, - Callable, - Generic, - Optional, - Sequence, - Tuple, - Type, - TypeVar, - cast, -) -from weakref import ref as weak_ref +from typing import TYPE_CHECKING, Generic, TypeVar, cast, overload +from weakref import WeakSet from .dbus_common_elements import ( - DbusBindedAsync, - DbusSingalCommon, - DbusSomethingAsync, + DbusBoundAsync, + DbusLocalObjectMeta, + DbusMemberAsync, + DbusRemoteObjectMeta, + DbusSignalCommon, ) -from .dbus_common_funcs import get_default_bus -from .sd_bus_internals import SdBus, SdBusMessage - -T = TypeVar('T') - +from .default_bus import get_default_bus if TYPE_CHECKING: + from collections.abc import Callable, Sequence + from typing import Any, Optional, Union + from .dbus_proxy_async_interface_base import DbusInterfaceBaseAsync + from .sd_bus_internals import SdBus, SdBusMessage, SdBusSlot -class DbusSignalAsync(DbusSomethingAsync, DbusSingalCommon, Generic[T]): +T = TypeVar('T') - def __get__(self, - obj: Optional[DbusInterfaceBaseAsync], - obj_class: Optional[Type[DbusInterfaceBaseAsync]] = None, - ) -> DbusSignalBinded[T]: - return DbusSignalBinded(self, obj) +class DbusSignalAsync(DbusMemberAsync, DbusSignalCommon, Generic[T]): -class DbusSignalBinded(Generic[T], DbusBindedAsync): - def __init__(self, - dbus_signal: DbusSignalAsync[T], - interface: Optional[DbusInterfaceBaseAsync]): - self.dbus_signal = dbus_signal - self.interface_ref = ( - weak_ref(interface) - if interface is not None - else None + def __init__( + self, + signal_name: Optional[str], + signal_signature: str, + args_names: Sequence[str], + flags: int, + original_method: FunctionType + ): + super().__init__( + signal_name, + signal_signature, + args_names, + flags, + original_method, ) - self.__doc__ = dbus_signal.__doc__ + self.local_callbacks: WeakSet[Callable[[T], Any]] = WeakSet() + + @overload + def __get__( + self, + obj: None, + obj_class: type[DbusInterfaceBaseAsync], + ) -> DbusSignalAsync[T]: + ... + + @overload + def __get__( + self, + obj: DbusInterfaceBaseAsync, + obj_class: type[DbusInterfaceBaseAsync], + ) -> DbusBoundSignalAsyncBase[T]: + ... + + def __get__( + self, + obj: Optional[DbusInterfaceBaseAsync], + obj_class: Optional[type[DbusInterfaceBaseAsync]] = None, + ) -> Union[DbusBoundSignalAsyncBase[T], DbusSignalAsync[T]]: + if obj is not None: + dbus_meta = obj._dbus + if isinstance(dbus_meta, DbusRemoteObjectMeta): + return DbusProxySignalAsync(self, dbus_meta) + else: + return DbusLocalSignalAsync(self, dbus_meta) + else: + return self - async def _get_dbus_queue(self) -> Queue[SdBusMessage]: - assert self.interface_ref is not None, ( - "Called method from class?" - ) - interface = self.interface_ref() - assert interface is not None - - assert interface._attached_bus is not None - assert interface._remote_service_name is not None - assert interface._remote_object_path is not None - assert self.dbus_signal.interface_name is not None - assert self.dbus_signal.signal_name is not None - - return await interface._attached_bus.get_signal_queue_async( - interface._remote_service_name, - interface._remote_object_path, - self.dbus_signal.interface_name, - self.dbus_signal.signal_name, - ) + async def catch_anywhere( + self, + service_name: str, + bus: Optional[SdBus] = None, + ) -> AsyncIterable[tuple[str, T]]: + if bus is None: + bus = get_default_bus() - def _cleanup_local_queue( - self, - queue_ref: weak_ref[Queue[T]]) -> None: - assert self.interface_ref is not None, ( - "Called method from class?" + message_queue: Queue[SdBusMessage] = Queue() + + match_slot = await bus.match_signal_async( + service_name, + None, + self.interface_name, + self.signal_name, + message_queue.put_nowait, ) - interface = self.interface_ref() - assert interface is not None - interface._local_signal_queues[self.dbus_signal].remove(queue_ref) + with closing(match_slot): + while True: + next_signal_message = await message_queue.get() + signal_path = next_signal_message.path + assert signal_path is not None + yield ( + signal_path, + cast(T, next_signal_message.get_contents()) + ) + - def _get_local_queue(self) -> Queue[T]: - assert self.interface_ref is not None, ( - "Called method from class?" - ) - interface = self.interface_ref() - assert interface is not None +class DbusBoundSignalAsyncBase(DbusBoundAsync, AsyncIterable[T], Generic[T]): + async def catch(self) -> AsyncIterator[T]: + raise NotImplementedError + yield cast(T, None) - try: - list_of_queues = interface._local_signal_queues[ - self.dbus_signal] - except KeyError: - list_of_queues = [] - interface._local_signal_queues[ - self.dbus_signal] = list_of_queues + __aiter__ = catch - new_queue: Queue[T] = Queue() + async def catch_anywhere( + self, + service_name: Optional[str] = None, + bus: Optional[SdBus] = None, + ) -> AsyncIterable[tuple[str, T]]: + raise NotImplementedError + yield "", cast(T, None) + + def emit(self, args: T) -> None: + raise NotImplementedError - list_of_queues.append(weak_ref(new_queue, self._cleanup_local_queue)) - return new_queue +class DbusProxySignalAsync(DbusBoundSignalAsyncBase[T]): + def __init__( + self, + dbus_signal: DbusSignalAsync[T], + proxy_meta: DbusRemoteObjectMeta, + ): + self.dbus_signal = dbus_signal + self.proxy_meta = proxy_meta - async def catch(self) -> AsyncGenerator[T, None]: - assert self.interface_ref is not None, ( - "Called method from class?" + self.__doc__ = dbus_signal.__doc__ + + async def _register_match_slot( + self, + bus: SdBus, + callback: Callable[[SdBusMessage], Any], + ) -> SdBusSlot: + return await bus.match_signal_async( + self.proxy_meta.service_name, + self.proxy_meta.object_path, + self.dbus_signal.interface_name, + self.dbus_signal.signal_name, + callback, ) - interface = self.interface_ref() - assert interface is not None - if interface._is_binded: - message_queue = await self._get_dbus_queue() + async def catch(self) -> AsyncIterator[T]: + message_queue: Queue[SdBusMessage] = Queue() + + match_slot = await self._register_match_slot( + self.proxy_meta.attached_bus, + message_queue.put_nowait, + ) + with closing(match_slot): while True: next_signal_message = await message_queue.get() yield cast(T, next_signal_message.get_contents()) - else: - data_queue = self._get_local_queue() - - while True: - next_data = await data_queue.get() - yield next_data __aiter__ = catch @@ -151,63 +187,82 @@ async def catch_anywhere( self, service_name: Optional[str] = None, bus: Optional[SdBus] = None, - ) -> AsyncGenerator[Tuple[str, T], None]: + ) -> AsyncIterable[tuple[str, T]]: + if bus is None: + bus = self.proxy_meta.attached_bus + if service_name is None: - if self.interface_ref is not None: - interface = self.interface_ref() - assert interface is not None - if interface._remote_service_name is None: - raise NotImplementedError( - 'catch_anywhere not implemented for ' - 'local objects' - ) - - service_name = interface._remote_service_name - else: - raise ValueError( - 'Called catch_anywhere from class ' - 'but service name was not provided' - ) + service_name = self.proxy_meta.service_name - if bus is None: - if self.interface_ref is not None: - interface = self.interface_ref() - assert interface is not None - assert interface._attached_bus is not None - bus = interface._attached_bus - else: - bus = get_default_bus() + message_queue: Queue[SdBusMessage] = Queue() - message_queue = await bus.get_signal_queue_async( + match_slot = await bus.match_signal_async( service_name, None, self.dbus_signal.interface_name, self.dbus_signal.signal_name, + message_queue.put_nowait, ) - while True: - next_signal_message = await message_queue.get() - signal_path = next_signal_message.path - assert signal_path is not None - yield ( - signal_path, - cast(T, next_signal_message.get_contents()) - ) - - def _emit_message(self, args: T) -> None: - assert self.interface_ref is not None, ( - "Called method from class?" - ) - interface = self.interface_ref() - assert interface is not None + with closing(match_slot): + while True: + next_signal_message = await message_queue.get() + signal_path = next_signal_message.path + assert signal_path is not None + yield ( + signal_path, + cast(T, next_signal_message.get_contents()) + ) + + def emit(self, args: T) -> None: + raise RuntimeError("Cannot emit signal from D-Bus proxy.") - assert interface._attached_bus is not None - assert interface._serving_object_path is not None - assert self.dbus_signal.interface_name is not None - assert self.dbus_signal.signal_name is not None - signal_message = interface._attached_bus.new_signal_message( - interface._serving_object_path, +class DbusLocalSignalAsync(DbusBoundSignalAsyncBase[T]): + def __init__( + self, + dbus_signal: DbusSignalAsync[T], + local_meta: DbusLocalObjectMeta, + ): + self.dbus_signal = dbus_signal + self.local_meta = local_meta + + self.__doc__ = dbus_signal.__doc__ + + async def catch(self) -> AsyncIterator[T]: + new_queue: Queue[T] = Queue() + + signal_callbacks = self.dbus_signal.local_callbacks + try: + put_method = new_queue.put_nowait + signal_callbacks.add(put_method) + while True: + next_data = await new_queue.get() + yield next_data + finally: + signal_callbacks.remove(put_method) + + __aiter__ = catch + + async def catch_anywhere( + self, + service_name: Optional[str] = None, + bus: Optional[SdBus] = None, + ) -> AsyncIterable[tuple[str, T]]: + raise NotImplementedError("TODO") + yield + + def _emit_dbus_signal(self, args: T) -> None: + attached_bus = self.local_meta.attached_bus + if attached_bus is None: + return + + serving_object_path = self.local_meta.serving_object_path + if serving_object_path is None: + return + + signal_message = attached_bus.new_signal_message( + serving_object_path, self.dbus_signal.interface_name, self.dbus_signal.signal_name, ) @@ -217,6 +272,8 @@ def _emit_message(self, args: T) -> None: isinstance(args, tuple)): signal_message.append_data( self.dbus_signal.signal_signature, *args) + elif self.dbus_signal.signal_signature == '' and args is None: + ... else: signal_message.append_data( self.dbus_signal.signal_signature, args) @@ -224,24 +281,10 @@ def _emit_message(self, args: T) -> None: signal_message.send() def emit(self, args: T) -> None: - assert self.interface_ref is not None, ( - "Called method from class?" - ) - interface = self.interface_ref() - assert interface is not None - - if interface._activated_interfaces: - self._emit_message(args) - - try: - list_of_queues = interface._local_signal_queues[self.dbus_signal] - except KeyError: - return + self._emit_dbus_signal(args) - for local_queue_ref in list_of_queues: - local_queue = local_queue_ref() - assert local_queue is not None - local_queue.put_nowait(args) + for callback in self.dbus_signal.local_callbacks: + callback(args) def dbus_signal_async( diff --git a/src/sdbus/dbus_proxy_sync_interface_base.py b/src/sdbus/dbus_proxy_sync_interface_base.py index ca7e737..6358c74 100644 --- a/src/sdbus/dbus_proxy_sync_interface_base.py +++ b/src/sdbus/dbus_proxy_sync_interface_base.py @@ -19,68 +19,125 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA from __future__ import annotations -from typing import Any, Dict, Optional, Set, Tuple, cast +from itertools import chain +from typing import TYPE_CHECKING +from weakref import WeakKeyDictionary, WeakValueDictionary from .dbus_common_elements import ( + DbusClassMeta, DbusInterfaceMetaCommon, - DbusSomethingAsync, - DbusSomethingSync, + DbusMemberAsync, + DbusMemberCommon, + DbusRemoteObjectMeta, ) -from .dbus_common_funcs import get_default_bus from .dbus_proxy_sync_method import DbusMethodSync from .dbus_proxy_sync_property import DbusPropertySync -from .sd_bus_internals import SdBus + +if TYPE_CHECKING: + from collections.abc import Iterable, Iterator + from typing import Any, Optional + + from .sd_bus_internals import SdBus + + +DBUS_CLASS_TO_META: WeakKeyDictionary[ + type, DbusClassMeta] = WeakKeyDictionary() +DBUS_INTERFACE_NAME_TO_CLASS: WeakValueDictionary[ + str, DbusInterfaceMetaSync] = WeakValueDictionary() class DbusInterfaceMetaSync(DbusInterfaceMetaCommon): - def __new__(cls, name: str, - bases: Tuple[type, ...], - namespace: Dict[str, Any], - interface_name: Optional[str] = None, - serving_enabled: bool = True, - ) -> DbusInterfaceMetaSync: - dbus_served_interfaces_names = ( - {interface_name} - if serving_enabled and interface_name is not None - else set() - ) - dbus_to_python_name_map: Dict[str, str] = {} - declared_interfaces = set() - # Set interface name - for key, value in namespace.items(): - assert not isinstance(value, DbusSomethingAsync), ( - "Can't mix async methods in sync interface." + @staticmethod + def _check_collisions( + new_class_name: str, + attr_names: set[str], + reserved_attr_names: set[str], + ) -> None: + + possible_collisions = attr_names & reserved_attr_names + if possible_collisions: + raise ValueError( + f"Interface {new_class_name!r} redefines reserved " + f"D-Bus attribute names: {possible_collisions!r}" + ) + + @staticmethod + def _collect_dbus_to_python_attr_names( + new_class_name: str, + base_classes: Iterable[type], + ) -> set[str]: + all_python_dbus_attrs: set[str] = set() + possible_collisions: set[str] = set() + + for c in base_classes: + dbus_meta = DBUS_CLASS_TO_META.get(c) + if dbus_meta is None: + continue + + base_python_dbus_attrs = set( + dbus_meta.python_attr_to_dbus_member.keys() + ) + + possible_collisions.update( + base_python_dbus_attrs & all_python_dbus_attrs + ) + + all_python_dbus_attrs.update( + base_python_dbus_attrs + ) + + if possible_collisions: + raise ValueError( + f"Interface {new_class_name!r} has a reserved D-Bus " + f"attribute name collision: {possible_collisions!r}" ) - if isinstance(value, DbusSomethingSync): - value.interface_name = interface_name - value.serving_enabled = serving_enabled - declared_interfaces.add(key) + return all_python_dbus_attrs - if isinstance(value, DbusMethodSync): - dbus_to_python_name_map[value.method_name] = key - elif isinstance(value, DbusPropertySync): - dbus_to_python_name_map[value.property_name] = key + @staticmethod + def _map_dbus_elements( + attr_name: str, + attr: Any, + meta: DbusClassMeta, + ) -> None: + if not isinstance(attr, DbusMemberCommon): + return - super_declared_interfaces = set() - for base in bases: - if issubclass(base, DbusInterfaceBase): - super_declared_interfaces.update( - base._dbus_declared_interfaces) + if isinstance(attr, DbusMemberAsync): + raise TypeError( + f"Can't mix async methods in sync interface: {attr_name!r}" + ) - dbus_to_python_name_map.update( - base._dbus_to_python_name_map - ) + if isinstance(attr, DbusMethodSync): + meta.dbus_member_to_python_attr[attr.method_name] = attr_name + meta.python_attr_to_dbus_member[attr_name] = attr.method_name + elif isinstance(attr, DbusPropertySync): + meta.dbus_member_to_python_attr[attr.property_name] = attr_name + meta.python_attr_to_dbus_member[attr_name] = attr.property_name + else: + raise TypeError(f"Unknown D-Bus element: {attr!r}") + + def __new__(cls, name: str, + bases: tuple[type, ...], + namespace: dict[str, Any], + interface_name: Optional[str] = None, + serving_enabled: bool = True, + ) -> DbusInterfaceMetaSync: - for key in super_declared_interfaces & namespace.keys(): - raise TypeError("Attempted to overload dbus definition" - " blocking interfaces do not support overloading") + if interface_name in DBUS_INTERFACE_NAME_TO_CLASS: + raise ValueError( + f"D-Bus interface of the name {interface_name!r} was " + "already created." + ) - namespace['_dbus_served_interfaces_names'] = \ - dbus_served_interfaces_names - namespace['_dbus_declared_interfaces'] = declared_interfaces - namespace['_dbus_to_python_name_map'] = dbus_to_python_name_map + all_mro_bases: set[type[Any]] = set( + chain.from_iterable(c.__mro__ for c in bases) + ) + reserved_attr_names = cls._collect_dbus_to_python_attr_names( + name, all_mro_bases, + ) + cls._check_collisions(name, set(namespace.keys()), reserved_attr_names) new_cls = super().__new__( cls, name, bases, namespace, @@ -88,22 +145,35 @@ def __new__(cls, name: str, serving_enabled, ) - return cast(DbusInterfaceMetaSync, new_cls) + if interface_name is not None: + dbus_class_meta = DbusClassMeta(interface_name, serving_enabled) + DBUS_CLASS_TO_META[new_cls] = dbus_class_meta + DBUS_INTERFACE_NAME_TO_CLASS[interface_name] = new_cls + + for attr_name, attr in namespace.items(): + cls._map_dbus_elements(attr_name, attr, dbus_class_meta) + + return new_cls class DbusInterfaceBase(metaclass=DbusInterfaceMetaSync): - _dbus_declared_interfaces: Set[str] - _dbus_serving_enabled: bool - _dbus_to_python_name_map: Dict[str, str] - _dbus_served_interfaces_names: Set[str] def __init__( - self, - service_name: str, - object_path: str, - bus: Optional[SdBus] = None, ) -> None: - self._remote_service_name = service_name - self._remote_object_path = object_path - self._attached_bus: SdBus = ( - bus if bus is not None - else get_default_bus()) + self, + service_name: str, + object_path: str, + bus: Optional[SdBus] = None, + ): + self._dbus = DbusRemoteObjectMeta(service_name, object_path, bus) + + @classmethod + def _dbus_iter_interfaces_meta( + cls, + ) -> Iterator[tuple[str, DbusClassMeta]]: + + for base in reversed(cls.__mro__): + meta = DBUS_CLASS_TO_META.get(base) + if meta is None: + continue + + yield meta.interface_name, meta diff --git a/src/sdbus/dbus_proxy_sync_interfaces.py b/src/sdbus/dbus_proxy_sync_interfaces.py index 7d9dc15..b14e73b 100644 --- a/src/sdbus/dbus_proxy_sync_interfaces.py +++ b/src/sdbus/dbus_proxy_sync_interfaces.py @@ -19,11 +19,14 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA from __future__ import annotations -from typing import Any, Dict, Literal, Tuple +from typing import TYPE_CHECKING from .dbus_proxy_sync_interface_base import DbusInterfaceBase from .dbus_proxy_sync_method import dbus_method +if TYPE_CHECKING: + from typing import Any, Literal + class DbusPeerInterface( DbusInterfaceBase, @@ -58,21 +61,23 @@ class DbusPropertiesInterface( ): @dbus_method('s', 'a{sv}', method_name='GetAll') def _properties_get_all( - self, interface_name: str) -> Dict[str, Tuple[str, Any]]: + self, interface_name: str) -> dict[str, tuple[str, Any]]: raise NotImplementedError def properties_get_all_dict( self, on_unknown_member: Literal['error', 'ignore', 'reuse'] = 'error', - ) -> Dict[str, Any]: - properties: Dict[str, Any] = {} + ) -> dict[str, Any]: + properties: dict[str, Any] = {} + + for interface_name, meta in self._dbus_iter_interfaces_meta(): + if not meta.serving_enabled: + continue - for interface_name in self._dbus_served_interfaces_names: - dbus_properties_data = self._properties_get_all( - interface_name) + dbus_properties_data = self._properties_get_all(interface_name) for member_name, variant in dbus_properties_data.items(): try: - python_name = self._dbus_to_python_name_map[member_name] + python_name = meta.dbus_member_to_python_attr[member_name] except KeyError: if on_unknown_member == 'error': raise @@ -89,9 +94,9 @@ def properties_get_all_dict( class DbusInterfaceCommon( - DbusPeerInterface, + DbusPropertiesInterface, DbusIntrospectable, - DbusPropertiesInterface): + DbusPeerInterface): ... @@ -102,5 +107,5 @@ class DbusObjectManagerInterface( ): @dbus_method(result_signature='a{oa{sa{sv}}}') def get_managed_objects( - self) -> Dict[str, Dict[str, Dict[str, Any]]]: + self) -> dict[str, dict[str, dict[str, Any]]]: raise NotImplementedError diff --git a/src/sdbus/dbus_proxy_sync_method.py b/src/sdbus/dbus_proxy_sync_method.py index 277f781..0ba58e1 100644 --- a/src/sdbus/dbus_proxy_sync_method.py +++ b/src/sdbus/dbus_proxy_sync_method.py @@ -21,43 +21,32 @@ from inspect import iscoroutinefunction from types import FunctionType -from typing import ( - TYPE_CHECKING, - Any, - Callable, - Optional, - Sequence, - Type, - TypeVar, - cast, -) +from typing import TYPE_CHECKING, TypeVar, cast from .dbus_common_elements import ( - DbusBindedSync, + DbusBoundSync, + DbusMemberSync, DbusMethodCommon, - DbusSomethingSync, ) -from .sd_bus_internals import SdBus - -DEFAULT_BUS: Optional[SdBus] = None - - -T_input = TypeVar('T_input') - if TYPE_CHECKING: + from collections.abc import Callable, Sequence + from typing import Any, Optional + from .dbus_proxy_sync_interface_base import DbusInterfaceBase +T = TypeVar('T') -class DbusMethodSync(DbusMethodCommon, DbusSomethingSync): + +class DbusMethodSync(DbusMethodCommon, DbusMemberSync): def __get__(self, obj: DbusInterfaceBase, - obj_class: Optional[Type[DbusInterfaceBase]] = None, + obj_class: Optional[type[DbusInterfaceBase]] = None, ) -> Callable[..., Any]: - return DbusMethodSyncBinded(self, obj) + return DbusLocalMethodSync(self, obj) -class DbusMethodSyncBinded(DbusBindedSync): +class DbusLocalMethodSync(DbusBoundSync): def __init__(self, dbus_method: DbusMethodSync, interface: DbusInterfaceBase): @@ -67,19 +56,19 @@ def __init__(self, self.__doc__ = dbus_method.__doc__ def _call_dbus_sync(self, *args: Any) -> Any: - assert self.dbus_method.interface_name is not None - new_call_message = self.interface._attached_bus. \ - new_method_call_message( - self.interface._remote_service_name, - self.interface._remote_object_path, + new_call_message = ( + self.interface._dbus.attached_bus.new_method_call_message( + self.interface._dbus.service_name, + self.interface._dbus.object_path, self.dbus_method.interface_name, self.dbus_method.method_name, ) + ) if args: new_call_message.append_data( self.dbus_method.input_signature, *args) - reply_message = self.interface._attached_bus.call( + reply_message = self.interface._dbus.attached_bus.call( new_call_message) return reply_message.get_contents() @@ -103,13 +92,13 @@ def dbus_method( result_signature: str = "", flags: int = 0, method_name: Optional[str] = None, -) -> Callable[[T_input], T_input]: +) -> Callable[[T], T]: assert not isinstance(input_signature, FunctionType), ( "Passed function to decorator directly. " "Did you forget () round brackets?" ) - def dbus_method_decorator(original_method: T_input) -> T_input: + def dbus_method_decorator(original_method: T) -> T: assert isinstance(original_method, FunctionType) assert not iscoroutinefunction(original_method), ( "Expected NON coroutine function. ", @@ -125,6 +114,6 @@ def dbus_method_decorator(original_method: T_input) -> T_input: flags=flags, ) - return cast(T_input, new_wrapper) + return cast(T, new_wrapper) return dbus_method_decorator diff --git a/src/sdbus/dbus_proxy_sync_property.py b/src/sdbus/dbus_proxy_sync_property.py index 7da6e51..20e79a1 100644 --- a/src/sdbus/dbus_proxy_sync_property.py +++ b/src/sdbus/dbus_proxy_sync_property.py @@ -21,28 +21,22 @@ from inspect import iscoroutinefunction from types import FunctionType -from typing import ( - TYPE_CHECKING, - Any, - Callable, - Generic, - Optional, - Type, - TypeVar, - cast, -) - -from .dbus_common_elements import DbusPropertyCommon, DbusSomethingSync -from .dbus_common_funcs import _check_sync_in_async_env - -T = TypeVar('T') +from typing import TYPE_CHECKING, Generic, TypeVar, cast +from .dbus_common_elements import DbusMemberSync, DbusPropertyCommon +from .dbus_common_funcs import _check_sync_in_async_env if TYPE_CHECKING: + from collections.abc import Callable + from typing import Any, Optional + from .dbus_proxy_sync_interface_base import DbusInterfaceBase -class DbusPropertySync(DbusPropertyCommon, DbusSomethingSync, Generic[T]): +T = TypeVar('T') + + +class DbusPropertySync(DbusPropertyCommon, DbusMemberSync, Generic[T]): def __init__( self, property_name: Optional[str], @@ -69,25 +63,24 @@ def __init__( def __get__(self, obj: DbusInterfaceBase, - obj_class: Optional[Type[DbusInterfaceBase]] = None, + obj_class: Optional[type[DbusInterfaceBase]] = None, ) -> T: assert _check_sync_in_async_env(), ( "Used sync __get__ method in async environment. " "This is probably an error as it will block " "other asyncio methods for considerable time." ) - assert self.interface_name is not None - new_call_message = obj._attached_bus. \ - new_property_get_message( - obj._remote_service_name, - obj._remote_object_path, + new_call_message = ( + obj._dbus.attached_bus.new_property_get_message( + obj._dbus.service_name, + obj._dbus.object_path, self.interface_name, self.property_name, ) + ) - reply_message = obj._attached_bus. \ - call(new_call_message) + reply_message = obj._dbus.attached_bus.call(new_call_message) return cast(T, reply_message.get_contents()[1]) def __set__(self, obj: DbusInterfaceBase, value: T) -> None: @@ -98,25 +91,21 @@ def __set__(self, obj: DbusInterfaceBase, value: T) -> None: ) if not self.property_signature: - raise AttributeError('Dbus property is read only') - - assert obj._attached_bus is not None - assert obj._remote_service_name is not None - assert obj._remote_object_path is not None - assert self.property_name is not None - assert self.interface_name is not None - new_call_message = obj._attached_bus. \ - new_property_set_message( - obj._remote_service_name, - obj._remote_object_path, + raise AttributeError('D-Bus property is read only') + + new_call_message = ( + obj._dbus.attached_bus.new_property_set_message( + obj._dbus.service_name, + obj._dbus.object_path, self.interface_name, self.property_name, ) + ) new_call_message.append_data( 'v', (self.property_signature, value)) - obj._attached_bus.call(new_call_message) + obj._dbus.attached_bus.call(new_call_message) def dbus_property( diff --git a/src/sdbus/default_bus.py b/src/sdbus/default_bus.py new file mode 100644 index 0000000..ad10068 --- /dev/null +++ b/src/sdbus/default_bus.py @@ -0,0 +1,204 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later + +# Copyright (C) 2020-2023 igo95862 + +# This file is part of python-sdbus + +# 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 __future__ import annotations + +import threading +from contextvars import ContextVar, Token +from logging import getLogger +from typing import TYPE_CHECKING + +from .sd_bus_internals import ( + NameAllowReplacementFlag, + NameQueueFlag, + NameReplaceExistingFlag, + sd_bus_open, +) + +if TYPE_CHECKING: + from typing import Optional + + from .sd_bus_internals import SdBus + +logger = getLogger(__name__) + + +class DefaultBusTLStorage(threading.local): + bus: Optional[SdBus] = None + + +bus_tls = DefaultBusTLStorage() +bus_contextvar: ContextVar[SdBus] = ContextVar("DEFAULT_BUS") + + +def _get_defaul_bus_tls() -> Optional[SdBus]: + return bus_tls.bus + + +def _set_default_bus_tls(new_bus: Optional[SdBus]) -> None: + bus_tls.bus = new_bus + + +def get_default_bus() -> SdBus: + """Get default bus. + + Returns context-local default bus if set or + thread-local otherwise. + + If no default bus is set initializes a new bus using + :py:func:`sdbus.sd_bus_open` and sets it as a thread-local + default bus. + """ + if (context_bus := bus_contextvar.get(None)) is not None: + return context_bus + + if (tls_bus := _get_defaul_bus_tls()) is not None: + return tls_bus + else: + new_bus = sd_bus_open() + logger.info( + "Created new default bus for thread %r", + threading.current_thread(), + ) + _set_default_bus_tls(new_bus) + return new_bus + + +def set_default_bus(new_default: SdBus) -> None: + """Set thread-local default bus. + + Should be called before creating any objects that will use + default bus. + + Default bus can be replaced but the change will only affect + newly created objects. + """ + _set_default_bus_tls(new_default) + + +def set_context_default_bus(new_default: SdBus) -> Token[SdBus]: + """Set context-local default bus. + + Should be called before creating any objects that will use + default bus. + + Default bus can be replaced but the change will only affect + newly created objects. + + Context-local default bus has higher priority over thread-local one + but has to be explicitly set. + + :returns: + Token that can be used to reset context bus back. + See ``contextvars`` documentation for details. + """ + return bus_contextvar.set(new_default) + + +def _prepare_request_name_flags( + allow_replacement: bool, + replace_existing: bool, + queue: bool, +) -> int: + return ( + (NameAllowReplacementFlag if allow_replacement else 0) + + + (NameReplaceExistingFlag if replace_existing else 0) + + + (NameQueueFlag if queue else 0) + ) + + +async def request_default_bus_name_async( + new_name: str, + allow_replacement: bool = False, + replace_existing: bool = False, + queue: bool = False, +) -> None: + r"""Asynchronously acquire a name on the default bus. + + :param new_name: + Name to acquire. + Must be a valid D-Bus service name. + :param allow_replacement: + If name was acquired allow other D-Bus peers to take away the name. + :param replace_existing: + If current name owner allows, take away the name. + :param queue: + Queue up for name acquisition. :py:exc:`.SdBusRequestNameInQueueError` + will be raised when successfully placed in queue. :py:meth:`Ownership + change signal ` should be monitored get notified when the name + was acquired. + :raises: :ref:`name-request-exceptions` and other D-Bus exceptions. + """ + default_bus = get_default_bus() + await default_bus.request_name_async( + new_name, + _prepare_request_name_flags( + allow_replacement, + replace_existing, + queue, + ) + ) + + +def request_default_bus_name( + new_name: str, + allow_replacement: bool = False, + replace_existing: bool = False, + queue: bool = False, +) -> None: + r"""Acquire a name on the default bus. + + Blocks until a reply is received from D-Bus daemon. + + :param new_name: + Name to acquire. + Must be a valid D-Bus service name. + :param allow_replacement: + If name was acquired allow other D-Bus peers to take away the name. + :param replace_existing: + If current name owner allows, take away the name. + :param queue: + Queue up for name acquisition. :py:exc:`.SdBusRequestNameInQueueError` + will be raised when successfully placed in queue. :py:meth:`Ownership + change signal ` should be monitored get notified when the name + was acquired. + :raises: :ref:`name-request-exceptions` and other D-Bus exceptions. + """ + default_bus = get_default_bus() + default_bus.request_name( + new_name, + _prepare_request_name_flags( + allow_replacement, + replace_existing, + queue, + ) + ) + + +__all__ = ( + "get_default_bus", + "set_default_bus", + "set_context_default_bus", + "request_default_bus_name_async", + "request_default_bus_name", +) diff --git a/src/sdbus/exceptions.py b/src/sdbus/exceptions.py new file mode 100644 index 0000000..7dfb403 --- /dev/null +++ b/src/sdbus/exceptions.py @@ -0,0 +1,107 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later + +# Copyright (C) 2023 igo95862 + +# This file is part of python-sdbus + +# 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 __future__ import annotations + +from .dbus_exceptions import ( + DbusAccessDeniedError, + DbusAddressInUseError, + DbusAuthFailedError, + DbusBadAddressError, + DbusDisconnectedError, + DbusFailedError, + DbusFileExistsError, + DbusFileNotFoundError, + DbusInconsistentMessageError, + DbusInteractiveAuthorizationRequiredError, + DbusInvalidArgsError, + DbusInvalidFileContentError, + DbusInvalidSignatureError, + DbusIOError, + DbusLimitsExceededError, + DbusMatchRuleInvalidError, + DbusMatchRuleNotFound, + DbusNameHasNoOwnerError, + DbusNoMemoryError, + DbusNoNetworkError, + DbusNoReplyError, + DbusNoServerError, + DbusNotSupportedError, + DbusPropertyReadOnlyError, + DbusServiceUnknownError, + DbusTimeoutError, + DbusUnixProcessIdUnknownError, + DbusUnknownInterfaceError, + DbusUnknownMethodError, + DbusUnknownObjectError, + DbusUnknownPropertyError, +) +from .sd_bus_internals import ( + SdBusBaseError, + SdBusLibraryError, + SdBusRequestNameAlreadyOwnerError, + SdBusRequestNameError, + SdBusRequestNameExistsError, + SdBusRequestNameInQueueError, + SdBusUnmappedMessageError, + map_exception_to_dbus_error, +) + +__all__ = ( + 'DbusAccessDeniedError', + 'DbusAddressInUseError', + 'DbusAuthFailedError', + 'DbusBadAddressError', + 'DbusDisconnectedError', + 'DbusFailedError', + 'DbusFileExistsError', + 'DbusFileNotFoundError', + 'DbusInconsistentMessageError', + 'DbusInteractiveAuthorizationRequiredError', + 'DbusInvalidArgsError', + 'DbusInvalidFileContentError', + 'DbusInvalidSignatureError', + 'DbusIOError', + 'DbusLimitsExceededError', + 'DbusMatchRuleInvalidError', + 'DbusMatchRuleNotFound', + 'DbusNameHasNoOwnerError', + 'DbusNoMemoryError', + 'DbusNoNetworkError', + 'DbusNoReplyError', + 'DbusNoServerError', + 'DbusNotSupportedError', + 'DbusPropertyReadOnlyError', + 'DbusServiceUnknownError', + 'DbusTimeoutError', + 'DbusUnixProcessIdUnknownError', + 'DbusUnknownInterfaceError', + 'DbusUnknownMethodError', + 'DbusUnknownObjectError', + 'DbusUnknownPropertyError', + 'map_exception_to_dbus_error', + + 'SdBusBaseError', + 'SdBusLibraryError', + 'SdBusRequestNameAlreadyOwnerError', + 'SdBusRequestNameError', + 'SdBusRequestNameExistsError', + 'SdBusRequestNameInQueueError', + 'SdBusUnmappedMessageError', +) diff --git a/src/sdbus/interface_generator.py b/src/sdbus/interface_generator.py index 273f53c..12bb577 100644 --- a/src/sdbus/interface_generator.py +++ b/src/sdbus/interface_generator.py @@ -20,11 +20,17 @@ from __future__ import annotations from pathlib import Path -from typing import Dict, Iterable, Iterator, List, Optional, Tuple, Union -from xml.etree.ElementTree import Element +from typing import TYPE_CHECKING from xml.etree.ElementTree import fromstring as etree_from_str from xml.etree.ElementTree import parse as etree_from_file +from .dbus_common_funcs import snake_case_to_camel_case + +if TYPE_CHECKING: + from collections.abc import Iterable, Iterator + from typing import Literal, Optional, Union + from xml.etree.ElementTree import Element + def _camel_case_to_snake_case_generator(camel: str) -> Iterator[str]: i = iter(camel) @@ -37,9 +43,12 @@ def _camel_case_to_snake_case_generator(camel: str) -> Iterator[str]: yield first_char.lower() + last_character = first_char + # Yield every character # if upper is encountered - # yield _ and lower + # yield _ only if previous character + # was not already uppercase or underscore while True: try: c = next(i) @@ -47,11 +56,15 @@ def _camel_case_to_snake_case_generator(camel: str) -> Iterator[str]: return if c.isupper(): - yield '_' + if not last_character.isupper() and not last_character == "_": + yield '_' + yield c.lower() else: yield c + last_character = c + def camel_case_to_snake_case(camel: str) -> str: return ''.join(_camel_case_to_snake_case_generator(camel)) @@ -123,11 +136,11 @@ def typing_basic(cls, char: str) -> str: @staticmethod def typing_into_tuple(typing_iter: Iterable[str]) -> str: - return f"Tuple[{', '.join(typing_iter)}]" + return f"tuple[{', '.join(typing_iter)}]" @staticmethod def slice_container(dbus_sig_iter: Iterator[str], peek_str: str) -> str: - accumulator: List[str] = [peek_str] + accumulator: list[str] = [peek_str] round_braces_count = 0 curly_braces_count = 0 @@ -167,8 +180,8 @@ def slice_container(dbus_sig_iter: Iterator[str], peek_str: str) -> str: return ''.join(accumulator) @classmethod - def split_sig(cls, sig: str) -> List[str]: - completes: List[str] = [] + def split_sig(cls, sig: str) -> list[str]: + completes: list[str] = [] sig_iter = iter(sig) @@ -206,18 +219,18 @@ def typing_complete(cls, complete_sig: str) -> str: dict_value_sig = complete_sig[3:-1] dict_value_typing = cls.typing_complete(dict_value_sig) - return f"Dict[{dict_key_typing}, {dict_value_typing}]" + return f"dict[{dict_key_typing}, {dict_value_typing}]" elif complete_sig.startswith('a'): array_completes = cls.split_sig(complete_sig[1:]) if len(array_completes) != 1: raise ValueError("Array does not have only " - "one complete type: {array_completes}") + f"one complete type: {array_completes}") array_single_complete = array_completes[0] - return f"List[{cls.typing_complete(array_single_complete)}]" + return f"list[{cls.typing_complete(array_single_complete)}]" elif complete_sig.startswith('('): if complete_sig[-1] != ')': raise ValueError(f"Malformed struct {complete_sig}") @@ -230,7 +243,7 @@ def typing_complete(cls, complete_sig: str) -> str: return cls.typing_basic(complete_sig) @classmethod - def result_typing(cls, result_args: List[str]) -> str: + def result_typing(cls, result_args: list[str]) -> str: result_len = len(result_args) if result_len == 0: @@ -239,7 +252,7 @@ def result_typing(cls, result_args: List[str]) -> str: return cls.typing_complete(result_args[0]) else: return cls.typing_into_tuple( - (cls.typing_complete(x) for x in result_args) + cls.typing_complete(x) for x in result_args ) @classmethod @@ -254,15 +267,18 @@ def __init__(self, element: Element): self.python_name = camel_case_to_snake_case(self.method_name) self.is_deprecated = False - self.is_unpriveledged = False + self.is_priveledged = False self.iter_sub_elements(element) + def _can_use_unpivileged(self) -> bool: + return True + def _flags_iter(self) -> Iterator[str]: if self.is_deprecated: yield 'DbusDeprecatedFlag' - if self.is_unpriveledged: + if not self.is_priveledged and self._can_use_unpivileged(): yield 'DbusUnprivilegedFlag' @property @@ -279,7 +295,7 @@ def _parse_annotation_data(self, if annotation_name == 'org.freedesktop.DBus.Deprecated': self.is_deprecated = parse_str_bool(annotation_value) elif annotation_name == 'org.freedesktop.systemd1.Privileged': - self.is_unpriveledged = parse_str_bool(annotation_value) + self.is_priveledged = parse_str_bool(annotation_value) else: ... @@ -303,6 +319,10 @@ def iter_sub_elements(self, element: Element) -> None: raise ValueError( 'Uknown member annotation tag: ', tag) + @property + def wants_rename(self) -> bool: + return self.method_name != snake_case_to_camel_case(self.python_name) + class DbusArgsIntrospection: def __init__(self, element: Element): @@ -331,7 +351,7 @@ def typing(self) -> str: return DbusSigToTyping.typing_complete(self.dbus_type) def __repr__(self) -> str: - return (f"Dbus Arg: {self.name}, " + return (f"D-Bus Arg: {self.name}, " f"type: {self.dbus_type}, " f"is input: {self.is_input}") @@ -343,8 +363,8 @@ def __init__(self, element: Element): self.is_no_reply = False - self.input_args: List[DbusArgsIntrospection] = [] - self.result_args: List[DbusArgsIntrospection] = [] + self.input_args: list[DbusArgsIntrospection] = [] + self.result_args: list[DbusArgsIntrospection] = [] super().__init__(element) @@ -366,19 +386,19 @@ def _parse_arg(self, arg: Element) -> None: @property def dbus_input_signature(self) -> str: return ''.join( - (x.dbus_type for x in self.input_args) + x.dbus_type for x in self.input_args ) @property def dbus_result_signature(self) -> str: return ''.join( - (x.dbus_type if not x.is_input else '' - for x in self.result_args) + x.dbus_type if not x.is_input else '' + for x in self.result_args ) @property - def args_names_and_typing(self) -> List[Tuple[str, str]]: - arg_names: List[Tuple[str, str]] = [] + def args_names_and_typing(self) -> list[tuple[str, str]]: + arg_names: list[tuple[str, str]] = [] for i, input_arg in enumerate(self.input_args): if input_arg.name is not None: @@ -395,16 +415,25 @@ def result_typing(self) -> str: return DbusSigToTyping.result_typing( [x.dbus_type for x in self.result_args]) + @property + def is_results_args_valid_names(self) -> bool: + return all(r.name is not None for r in self.result_args) + + @property + def result_args_names_repr(self) -> str: + return repr(tuple(r.name for r in self.result_args)) + def __repr__(self) -> str: - return (f"Dbus Method: {self.method_name}, " + return (f"D-Bus Method: {self.method_name}, " f"args: {self.args_names_and_typing}, " f"result: {self.dbus_result_signature}") class DbusPropertyIntrospection(DbusMemberAbstract): - _EMITS_CHANGED_MAP: Dict[str, Optional[str]] = { - 'true': 'DbusPropertyEmitsChangeFlag', - 'false': None, + _EMITS_CHANGED_MAP: dict[ + Union[bool, Literal['const', 'invalidates']], str + ] = { + True: 'DbusPropertyEmitsChangeFlag', 'invalidates': 'DbusPropertyEmitsInvalidationFlag', 'const': 'DbusPropertyConstFlag', } @@ -415,22 +444,29 @@ def __init__(self, element: Element): self.dbus_signature = element.attrib['type'] - self.emits_changed: Optional[str] = None + self.emits_changed: Union[bool, Literal['const', 'invalidates']] = True self.is_explicit = False access_type = element.attrib['access'] - if access_type == 'readwrite': + if access_type == 'readwrite' or access_type == 'write': self.is_read_only = False elif access_type == 'read': self.is_read_only = True else: - raise ValueError(f"Unknown property access {self.is_read_only}") + raise ValueError(f"Unknown property access {access_type}") super().__init__(element) + def _can_use_unpivileged(self) -> bool: + # Only properties that have setters defined can use the + # unprivileged flags. The code generator does NOT generate + # setters. + return False + def _flags_iter(self) -> Iterator[str]: - if self.emits_changed is not None: - yield self.emits_changed + emits_changed_str = self._EMITS_CHANGED_MAP.get(self.emits_changed) + if emits_changed_str is not None: + yield emits_changed_str yield from super()._flags_iter() @@ -440,11 +476,17 @@ def _parse_annotation_data(self, if annotation_name == ('org.freedesktop.DBus.Property' '.EmitsChangedSignal'): - if annotation_value not in self._EMITS_CHANGED_MAP: + if annotation_value == 'true': + self.emits_changed = True + elif annotation_value == 'false': + self.emits_changed = False + elif annotation_value == 'const': + self.emits_changed = 'const' + elif annotation_value == 'invalidates': + self.emits_changed = 'invalidates' + else: raise ValueError('Unknown EmitsChanged value', annotation_value) - - self.emits_changed = self._EMITS_CHANGED_MAP[annotation_value] elif annotation_name == 'org.freedesktop.systemd1.Explicit': self.is_explicit = parse_str_bool(annotation_value) @@ -460,9 +502,12 @@ def __init__(self, element: Element): if element.tag != 'signal': raise ValueError(f"Expected signal tag, got {element.tag}") - self.args: List[DbusArgsIntrospection] = [] + self.args: list[DbusArgsIntrospection] = [] super().__init__(element) + def _can_use_unpivileged(self) -> bool: + return False + def _parse_arg(self, arg: Element) -> None: new_arg = DbusArgsIntrospection(arg) @@ -473,13 +518,21 @@ def _parse_arg(self, arg: Element) -> None: @property def dbus_signature(self) -> str: - return ''.join((x.dbus_type for x in self.args)) + return ''.join(x.dbus_type for x in self.args) @property def typing(self) -> str: return DbusSigToTyping.result_typing( [x.dbus_type for x in self.args]) + @property + def is_args_valid_names(self) -> bool: + return all(a.name is not None for a in self.args) + + @property + def args_names_repr(self) -> str: + return repr(tuple(a.name for a in self.args)) + class DbusInterfaceIntrospection: def __init__(self, element: Element): @@ -493,9 +546,9 @@ def __init__(self, element: Element): self.is_deprecated = False self.c_name: Optional[str] = None - self.methods: List[DbusMethodInrospection] = [] - self.properties: List[DbusPropertyIntrospection] = [] - self.signals: List[DbusSignalIntrospection] = [] + self.methods: list[DbusMethodInrospection] = [] + self.properties: list[DbusPropertyIntrospection] = [] + self.signals: list[DbusSignalIntrospection] = [] for dbus_member in element: if dbus_member.tag == 'method': self.methods.append(DbusMethodInrospection(dbus_member)) @@ -514,98 +567,258 @@ def __init__(self, element: Element): else: ... else: - raise ValueError(f'Unknown dbus member {dbus_member}') + raise ValueError(f'Unknown D-Bus member {dbus_member}') - def generate_interface_class(self) -> str: - from jinja2 import Environment as JinjaEnv + @property + def has_members(self) -> bool: + return any((self.methods, self.properties, self.signals)) - env = JinjaEnv(trim_blocks=True) - template = env.from_string(async_interface_template_txt) - return template.render(interface=self) +SKIP_INTERFACES = { + 'org.freedesktop.DBus.Properties', + 'org.freedesktop.DBus.Introspectable', + 'org.freedesktop.DBus.Peer', + 'org.freedesktop.DBus.ObjectManager', +} -async_import_header_txt = """ +INTERFACE_TEMPLATES: dict[str, str] = { + "generic_no_members": """\ +... # Interface has no members +""", + "generic_method_flags": ( + """\ +{% if method.dbus_input_signature %} +input_signature="{{ method.dbus_input_signature }}", +{% endif %} +{% if method.dbus_result_signature %} +result_signature="{{ method.dbus_result_signature }}", +{% endif %} +{% if method.flags_str %} +flags={{ method.flags_str }}, +{% endif %} +{% if method.wants_rename %} +method_name="{{method.method_name}}", +{% endif %} +""" + ), + "generic_property_flags": ( + """\ +{% if a_property.dbus_signature %} +property_signature="{{ a_property.dbus_signature }}", +{% endif %} +{% if a_property.flags_str %} +flags={{ a_property.flags_str }}, +{% endif %} +{% if a_property.wants_rename %} +property_name="{{a_property.method_name}}", +{% endif %} +""" + ), + "generic_header": """\ from __future__ import annotations -from typing import Any, Dict, List, Tuple +from typing import Any -from sdbus import (DbusDeprecatedFlag, DbusInterfaceCommonAsync, - DbusNoReplyFlag, DbusPropertyConstFlag, - DbusPropertyEmitsChangeFlag, - DbusPropertyEmitsInvalidationFlag, DbusPropertyExplicitFlag, - DbusUnprivilegedFlag, dbus_method_async, - dbus_property_async, dbus_signal_async) -""" +""", + "async_imports_header": """from sdbus import ( + DbusDeprecatedFlag, + DbusInterfaceCommonAsync, + DbusNoReplyFlag, + DbusPropertyConstFlag, + DbusPropertyEmitsChangeFlag, + DbusPropertyEmitsInvalidationFlag, + DbusPropertyExplicitFlag, + DbusUnprivilegedFlag, + dbus_method_async, + dbus_property_async, + dbus_signal_async, +) + +""", + "async_main": ( + """\ +{% if include_import_header %} + {% include 'generic_header' %} + + {% include 'async_imports_header' %} +{% endif %} -async_interface_template_txt = """ +{% for interface in interfaces %} + {% include 'async_interface' %} +{% endfor %} +""" + ), + "async_interface": ( + """\ class {{ interface.python_name }}( DbusInterfaceCommonAsync, - interface_name='{{ interface.interface_name }}', + interface_name="{{ interface.interface_name }}", ): -{% for method in interface.methods %} +{% filter indent(first=True) %} + {% if interface.has_members %} + {% for method in interface.methods %} + {% include 'async_method' %} - @dbus_method_async( -{% if method.dbus_input_signature %} - input_signature='{{ method.dbus_input_signature }}', -{% endif %} -{% if method.dbus_result_signature %} - result_signature='{{ method.dbus_result_signature }}', -{% endif %} -{% if method.flags_str %} - flags={{ method.flags_str }}, + {% endfor %} + {% for a_property in interface.properties %} + {% include 'async_property' %} + + {% endfor %} + {% for signal in interface.signals %} + {% include 'async_signal' %} + + {% endfor %} + {% else %} + {% include 'generic_no_members' %} + + {% endif %} +{% endfilter %} +""" + ), + "async_method": ( + """\ +@dbus_method_async( +{% filter indent(first=True) %} + {% include 'generic_method_flags' %} +{% endfilter %} +{% if method.is_results_args_valid_names %} + result_args_names={{method.result_args_names_repr}}, {% endif %} - ) - async def {{ method.python_name }}( - self, +) +async def {{ method.python_name }}( + self, {% for arg_name, arg_type in method.args_names_and_typing %} - {{ arg_name }}: {{ arg_type }}, -{% endfor %} - ) -> {{ method.result_typing }}: - raise NotImplementedError + {{ arg_name }}: {{ arg_type }}, {% endfor %} -{% for a_property in interface.properties %} +) -> {{ method.result_typing }}: + raise NotImplementedError - @dbus_property_async( -{% if a_property.dbus_signature %} - property_signature='{{ a_property.dbus_signature }}', -{% endif %} -{% if a_property.flags_str %} - flags={{ a_property.flags_str }}, -{% endif %} - ) - def {{ a_property.python_name }}(self) -> {{ a_property.typing }}: - raise NotImplementedError -{% endfor %} -{% for signal in interface.signals %} +""" + ), + "async_property": ( + """\ +@dbus_property_async( +{% filter indent(first=True) %} + {% include 'generic_property_flags' %} +{% endfilter %} +) +def {{ a_property.python_name }}(self) -> {{ a_property.typing }}: + raise NotImplementedError - @dbus_signal_async( +""" + ), + "async_signal": ( + """\ +@dbus_signal_async( {% if signal.dbus_signature %} - signal_signature='{{ signal.dbus_signature }}', + signal_signature="{{ signal.dbus_signature }}", +{% endif %} +{% if signal.is_args_valid_names %} + signal_args_names={{signal.args_names_repr}}, {% endif %} {% if signal.flags_str %} - flags={{ signal.flags_str }}, + flags={{ signal.flags_str }}, {% endif %} - ) - def {{ signal.python_name }}(self) -> {{ signal.typing }}: - raise NotImplementedError +{% if signal.wants_rename %} + signal_name="{{signal.method_name}}", +{% endif %} +) +def {{ signal.python_name }}(self) -> {{ signal.typing }}: + raise NotImplementedError + +""" + ), + "blocking_imports_header": """\ +from sdbus import ( + DbusDeprecatedFlag, + DbusInterfaceCommon, + DbusNoReplyFlag, + DbusPropertyConstFlag, + DbusPropertyEmitsChangeFlag, + DbusPropertyEmitsInvalidationFlag, + DbusPropertyExplicitFlag, + DbusUnprivilegedFlag, + dbus_method, + dbus_property, +) + +""", + "blocking_main": ( + """\ +{% if include_import_header %} + {% include 'generic_header' %} + + {% include 'blocking_imports_header' %} +{% endif %} + +{% for interface in interfaces %} + + {% include 'blocking_interface' %} {% endfor %} """ + ), + "blocking_interface": ( + """\ +class {{ interface.python_name }}( + DbusInterfaceCommon, + interface_name="{{ interface.interface_name }}", +): +{% filter indent(first=True) %} + {% if interface.has_members %} + {% for method in interface.methods %} + {% include 'blocking_method' %} -SKIP_INTERFACES = { - 'org.freedesktop.DBus.Properties', - 'org.freedesktop.DBus.Introspectable', - 'org.freedesktop.DBus.Peer', - 'org.freedesktop.DBus.ObjectManager', + {% endfor %} + {% for a_property in interface.properties %} + {% include 'blocking_property' %} + + {% endfor %} + {% else %} + {% include 'generic_no_members' %} + + {% endif %} +{% endfilter %} +""" + ), + "blocking_method": ( + """\ +@dbus_method( +{% filter indent(first=True) %} + {% include 'generic_method_flags' %} +{% endfilter %} +) +def {{ method.python_name }}( + self, +{% for arg_name, arg_type in method.args_names_and_typing %} + {{ arg_name }}: {{ arg_type }}, +{% endfor %} +) -> {{ method.result_typing }}: + raise NotImplementedError + +""" + ), + "blocking_property": ( + """\ +@dbus_property( +{% filter indent(first=True) %} + {% include 'generic_property_flags' %} +{% endfilter %} +) +def {{ a_property.python_name }}(self) -> {{ a_property.typing }}: + raise NotImplementedError + +""" + ), } def xml_to_interfaces_introspection( - root: Element) -> List[DbusInterfaceIntrospection]: + root: Element) -> list[DbusInterfaceIntrospection]: - list_of_interface_introspection: List[DbusInterfaceIntrospection] = [] + list_of_interface_introspection: list[DbusInterfaceIntrospection] = [] if root.tag != 'node': raise ValueError(f"Expected node tag got {root.tag}") @@ -624,28 +837,38 @@ def xml_to_interfaces_introspection( def interfaces_from_file(filename_or_path: Union[str, Path] - ) -> List[DbusInterfaceIntrospection]: + ) -> list[DbusInterfaceIntrospection]: etree = etree_from_file(filename_or_path) return xml_to_interfaces_introspection(etree.getroot()) -def interfaces_from_str(xml_str: str) -> List[DbusInterfaceIntrospection]: +def interfaces_from_str(xml_str: str) -> list[DbusInterfaceIntrospection]: etree = etree_from_str(xml_str) return xml_to_interfaces_introspection(etree) -def generate_async_py_file( - interfaces: List[DbusInterfaceIntrospection], - include_import_header: bool = True) -> str: +def generate_py_file( + interfaces: list[DbusInterfaceIntrospection], + include_import_header: bool = True, + do_async: bool = True, +) -> str: - interfaces_definitions = '\n'.join( - (x.generate_interface_class() for x in interfaces)) + from jinja2 import DictLoader + from jinja2 import Environment as JinjaEnv - if include_import_header: - return async_import_header_txt + interfaces_definitions - else: - return interfaces_definitions + template_name = "async_main" if do_async else "blocking_main" + + env = JinjaEnv( + loader=DictLoader(INTERFACE_TEMPLATES), + trim_blocks=True, + lstrip_blocks=True, + autoescape=False, + ) + return env.get_template(template_name).render( + interfaces=interfaces, + include_import_header=include_import_header, + ) diff --git a/src/sdbus/sd_bus_internals.c b/src/sdbus/sd_bus_internals.c index c7c887a..b97f444 100644 --- a/src/sdbus/sd_bus_internals.c +++ b/src/sdbus/sd_bus_internals.c @@ -21,26 +21,32 @@ #include "sd_bus_internals.h" // Python functions and objects -PyObject* unmapped_error_exception = NULL; -PyObject* dbus_error_to_exception_dict = NULL; -PyObject* exception_to_dbus_error_dict = NULL; -PyObject* exception_base = NULL; -PyObject* exception_lib = NULL; PyObject* asyncio_get_running_loop = NULL; -PyObject* asyncio_queue_class = NULL; PyObject* is_coroutine_function = NULL; // Str objects PyObject* set_result_str = NULL; PyObject* set_exception_str = NULL; -PyObject* put_no_wait_str = NULL; PyObject* add_reader_str = NULL; PyObject* remove_reader_str = NULL; +PyObject* add_writer_str = NULL; +PyObject* remove_writer_str = NULL; PyObject* empty_str = NULL; PyObject* null_str = NULL; PyObject* extend_str = NULL; PyObject* append_str = NULL; PyObject* call_soon_str = NULL; PyObject* create_task_str = NULL; +// Exceptions +PyObject* exception_base = NULL; +PyObject* unmapped_error_exception = NULL; +PyObject* exception_lib = NULL; +PyObject* exception_request_name = NULL; // Base to any request name exception +PyObject* exception_request_name_in_queue = NULL; // Queued up to acquire name +PyObject* exception_request_name_exists = NULL; // Someone already owns the name +PyObject* exception_request_name_already_owner = NULL; // Already an owner of the name + +PyObject* dbus_error_to_exception_dict = NULL; +PyObject* exception_to_dbus_error_dict = NULL; // SdBusSlot @@ -50,6 +56,18 @@ static void SdBusSlot_dealloc(SdBusSlotObject* self) { SD_BUS_DEALLOC_TAIL; } +static PyObject* SdBusSlot_close(SdBusSlotObject* self, PyObject* Py_UNUSED(args)) { + sd_bus_slot_unref(self->slot_ref); + self->slot_ref = NULL; + + Py_RETURN_NONE; +} + +static PyMethodDef SdBusSlot_methods[] = { + {"close", (PyCFunction)SdBusSlot_close, METH_NOARGS, PyDoc_STR("Dereference sd-bus slot stopping any associated callbacks.")}, + {NULL, NULL, 0, NULL}, +}; + PyType_Spec SdBusSlotType = { .name = "sd_bus_internals.SdBusSlot", .basicsize = sizeof(SdBusSlotObject), @@ -59,12 +77,13 @@ PyType_Spec SdBusSlotType = { (PyType_Slot[]){ {Py_tp_new, PyType_GenericNew}, {Py_tp_dealloc, (destructor)SdBusSlot_dealloc}, + {Py_tp_methods, SdBusSlot_methods}, {0, NULL}, }, }; static PyModuleDef sd_bus_internals_module = { - PyModuleDef_HEAD_INIT, .m_name = "sd_bus_internals", .m_doc = "Sd bus internals module.", .m_methods = SdBusPyInternal_methods, .m_size = -1, + PyModuleDef_HEAD_INIT, .m_name = "sd_bus_internals", .m_doc = PyDoc_STR("Sd bus internals module."), .m_methods = SdBusPyInternal_methods, .m_size = -1, }; PyObject* SdBus_class = NULL; @@ -123,19 +142,39 @@ PyMODINIT_FUNC PyInit_sd_bus_internals(void) { SD_BUS_PY_INIT_ADD_OBJECT("SdBusLibraryError", library_exception); exception_lib = library_exception; + // Request name exceptions + PyObject* request_name_exception CLEANUP_PY_OBJECT = + CALL_PYTHON_AND_CHECK(PyErr_NewException("sd_bus_internals.SdBusRequestNameError", new_base_exception, NULL)); + SD_BUS_PY_INIT_ADD_OBJECT("SdBusRequestNameError", request_name_exception); + exception_request_name = request_name_exception; + // Request name but in queue + PyObject* request_name_in_queue_exception CLEANUP_PY_OBJECT = + CALL_PYTHON_AND_CHECK(PyErr_NewException("sd_bus_internals.SdBusRequestNameInQueueError", request_name_exception, NULL)); + SD_BUS_PY_INIT_ADD_OBJECT("SdBusRequestNameInQueueError", request_name_in_queue_exception); + exception_request_name_in_queue = request_name_in_queue_exception; + // Request name but someone already owns the name + PyObject* request_name_exists_exception CLEANUP_PY_OBJECT = + CALL_PYTHON_AND_CHECK(PyErr_NewException("sd_bus_internals.SdBusRequestNameExistsError", request_name_exception, NULL)); + SD_BUS_PY_INIT_ADD_OBJECT("SdBusRequestNameExistsError", request_name_exists_exception); + exception_request_name_exists = request_name_exists_exception; + // Request name but we already own the name + PyObject* request_name_already_owner_exception CLEANUP_PY_OBJECT = + CALL_PYTHON_AND_CHECK(PyErr_NewException("sd_bus_internals.SdBusRequestNameAlreadyOwnerError", request_name_exception, NULL)); + SD_BUS_PY_INIT_ADD_OBJECT("SdBusRequestNameAlreadyOwnerError", request_name_already_owner_exception); + exception_request_name_already_owner = request_name_already_owner_exception; + PyObject* asyncio_module = CALL_PYTHON_AND_CHECK(PyImport_ImportModule("asyncio")); asyncio_get_running_loop = CALL_PYTHON_AND_CHECK(PyObject_GetAttrString(asyncio_module, "get_running_loop")); - asyncio_queue_class = CALL_PYTHON_AND_CHECK(PyObject_GetAttrString(asyncio_module, "Queue")); - set_result_str = CALL_PYTHON_AND_CHECK(PyUnicode_FromString("set_result")); set_exception_str = CALL_PYTHON_AND_CHECK(PyUnicode_FromString("set_exception")); - put_no_wait_str = CALL_PYTHON_AND_CHECK(PyUnicode_FromString("put_nowait")); call_soon_str = CALL_PYTHON_AND_CHECK(PyUnicode_FromString("call_soon")); create_task_str = CALL_PYTHON_AND_CHECK(PyUnicode_FromString("create_task")); remove_reader_str = CALL_PYTHON_AND_CHECK(PyUnicode_FromString("remove_reader")); add_reader_str = CALL_PYTHON_AND_CHECK(PyUnicode_FromString("add_reader")); + add_writer_str = CALL_PYTHON_AND_CHECK(PyUnicode_FromString("add_writer")); + remove_writer_str = CALL_PYTHON_AND_CHECK(PyUnicode_FromString("remove_writer")); empty_str = CALL_PYTHON_AND_CHECK(PyUnicode_FromString("")); null_str = CALL_PYTHON_AND_CHECK(PyUnicode_FromStringAndSize("\0", 1)); extend_str = CALL_PYTHON_AND_CHECK(PyUnicode_FromString("extend")); @@ -154,6 +193,10 @@ PyMODINIT_FUNC PyInit_sd_bus_internals(void) { CALL_PYTHON_INT_CHECK(PyModule_AddIntConstant(m, "DbusPropertyExplicitFlag", SD_BUS_VTABLE_PROPERTY_EXPLICIT)); CALL_PYTHON_INT_CHECK(PyModule_AddIntConstant(m, "DbusSensitiveFlag", SD_BUS_VTABLE_SENSITIVE)); + CALL_PYTHON_INT_CHECK(PyModule_AddIntConstant(m, "NameAllowReplacementFlag", SD_BUS_NAME_ALLOW_REPLACEMENT)); + CALL_PYTHON_INT_CHECK(PyModule_AddIntConstant(m, "NameReplaceExistingFlag", SD_BUS_NAME_REPLACE_EXISTING)); + CALL_PYTHON_INT_CHECK(PyModule_AddIntConstant(m, "NameQueueFlag", SD_BUS_NAME_QUEUE)); + Py_INCREF(m); return m; } diff --git a/src/sdbus/sd_bus_internals.h b/src/sdbus/sd_bus_internals.h index a928e2b..71784b3 100644 --- a/src/sdbus/sd_bus_internals.h +++ b/src/sdbus/sd_bus_internals.h @@ -239,26 +239,32 @@ #endif // Python functions and objects -extern PyObject* unmapped_error_exception; -extern PyObject* dbus_error_to_exception_dict; -extern PyObject* exception_to_dbus_error_dict; -extern PyObject* exception_base; -extern PyObject* exception_lib; extern PyObject* asyncio_get_running_loop; -extern PyObject* asyncio_queue_class; extern PyObject* is_coroutine_function; // Str objects extern PyObject* set_result_str; extern PyObject* set_exception_str; -extern PyObject* put_no_wait_str; extern PyObject* add_reader_str; extern PyObject* remove_reader_str; +extern PyObject* add_writer_str; +extern PyObject* remove_writer_str; extern PyObject* empty_str; extern PyObject* null_str; extern PyObject* extend_str; extern PyObject* append_str; extern PyObject* call_soon_str; extern PyObject* create_task_str; +// Exceptions +extern PyObject* exception_base; +extern PyObject* unmapped_error_exception; +extern PyObject* exception_lib; +extern PyObject* exception_request_name; // Base to any request name exception +extern PyObject* exception_request_name_in_queue; // Queued up to acquire name +extern PyObject* exception_request_name_exists; // Someone already owns the name +extern PyObject* exception_request_name_already_owner; // Already an owner of the name + +extern PyObject* dbus_error_to_exception_dict; +extern PyObject* exception_to_dbus_error_dict; __attribute__((used)) static inline void _cleanup_char_ptr(const char** ptr) { if (*ptr != NULL) { @@ -326,7 +332,11 @@ extern PyObject* SdBusMessage_class; typedef struct { PyObject_HEAD; sd_bus* sd_bus_ref; - PyObject* reader_fd; + PyObject* bus_fd; + PyObject* loop; + PyObject* timer_fd; + int asyncio_watchers_last_state; + int timer_fd_int; } SdBusObject; extern PyType_Spec SdBusType; diff --git a/src/sdbus/sd_bus_internals.py b/src/sdbus/sd_bus_internals.py index c3d9a26..2981b17 100644 --- a/src/sdbus/sd_bus_internals.py +++ b/src/sdbus/sd_bus_internals.py @@ -1,6 +1,6 @@ # SPDX-License-Identifier: LGPL-2.1-or-later -# Copyright (C) 2020, 2021 igo95862 +# Copyright (C) 2020-2023 igo95862 # This file is part of python-sdbus @@ -19,27 +19,20 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA from __future__ import annotations -from asyncio import Future, Queue -from typing import ( - Any, - Callable, - Coroutine, - Dict, - List, - Optional, - Sequence, - Tuple, - Type, - Union, -) +from asyncio import Future +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from collections.abc import Callable, Sequence + from typing import Any, Optional, Union -DbusBasicTypes = Union[str, int, bytes, float, Any] -DbusStructType = Tuple[DbusBasicTypes, ...] -DbusDictType = Dict[DbusBasicTypes, DbusBasicTypes] -DbusVariantType = Tuple[str, DbusStructType] -DbusListType = List[DbusBasicTypes] -DbusCompleteTypes = Union[DbusBasicTypes, DbusStructType, - DbusDictType, DbusVariantType, DbusListType] + DbusBasicTypes = Union[str, int, bytes, float, Any] + DbusStructType = tuple[DbusBasicTypes, ...] + DbusDictType = dict[DbusBasicTypes, DbusBasicTypes] + DbusVariantType = tuple[str, DbusStructType] + DbusListType = list[DbusBasicTypes] + DbusCompleteTypes = Union[DbusBasicTypes, DbusStructType, + DbusDictType, DbusVariantType, DbusListType] __STUB_ERROR = ( 'Typing stub. You should never see this ' @@ -50,16 +43,18 @@ class SdBusSlot: """Holds reference to SdBus slot""" - ... + + def close(self) -> None: + raise NotImplementedError(__STUB_ERROR) class SdBusInterface: - method_list: List[object] - method_dict: Dict[bytes, object] - property_list: List[object] - property_get_dict: Dict[bytes, object] - property_set_dict: Dict[bytes, object] - signal_list: List[object] + method_list: list[object] + method_dict: dict[bytes, object] + property_list: list[object] + property_get_dict: dict[bytes, object] + property_set_dict: dict[bytes, object] + signal_list: list[object] def add_method( self, @@ -67,7 +62,7 @@ def add_method( signature: str, input_args_names: Sequence[str], result_signature: str, result_args_names: Sequence[str], flags: int, - callback: Callable[[SdBusMessage], Coroutine[Any, Any, None]], / + callback: Callable[[SdBusMessage], None], / ) -> None: raise NotImplementedError(__STUB_ERROR) @@ -90,6 +85,9 @@ def add_signal( ) -> None: raise NotImplementedError(__STUB_ERROR) + def _stop_export(self) -> None: + raise NotImplementedError(__STUB_ERROR) + class SdBusMessage: def append_data(self, signature: str, *args: DbusCompleteTypes) -> None: @@ -116,7 +114,7 @@ def seal(self) -> None: raise NotImplementedError(__STUB_ERROR) def get_contents(self - ) -> Tuple[DbusCompleteTypes, ...]: + ) -> tuple[DbusCompleteTypes, ...]: raise NotImplementedError(__STUB_ERROR) def create_reply(self) -> SdBusMessage: @@ -131,6 +129,9 @@ def create_error_reply( def send(self) -> None: raise NotImplementedError(__STUB_ERROR) + def parse_to_tuple(self) -> tuple[Any, ...]: + raise NotImplementedError(__STUB_ERROR) + expect_reply: bool = False destination: Optional[str] = None path: Optional[str] = None @@ -148,7 +149,7 @@ def call_async( /) -> Future[SdBusMessage]: raise NotImplementedError(__STUB_ERROR) - def drive(self) -> None: + def process(self) -> None: raise NotImplementedError(__STUB_ERROR) def get_fd(self) -> int: @@ -187,12 +188,12 @@ def add_interface(self, new_interface: SdBusInterface, object_path: str, interface_name: str, /) -> None: raise NotImplementedError(__STUB_ERROR) - def get_signal_queue_async( + def match_signal_async( self, senders_name: Optional[str], object_path: Optional[str], interface_name: Optional[str], member_name: Optional[str], - / - ) -> Future[Queue[SdBusMessage]]: + callback: Callable[[SdBusMessage], None], / + ) -> Future[SdBusSlot]: raise NotImplementedError(__STUB_ERROR) def request_name_async(self, name: str, flags: int, /) -> Future[None]: @@ -217,6 +218,7 @@ def start(self) -> None: raise NotImplementedError(__STUB_ERROR) address: Optional[str] = None + method_call_timeout_usec: int = 0 def sd_bus_open() -> SdBus: @@ -251,12 +253,12 @@ def decode_object_path(prefix: str, full_path: str) -> str: raise NotImplementedError(__STUB_ERROR) -def map_exception_to_dbus_error(exc: Type[Exception], +def map_exception_to_dbus_error(exc: type[Exception], dbus_error_name: str, /) -> None: ... # We want to be able to generate docs without module -def add_exception_mapping(exc: Exception, /) -> None: +def add_exception_mapping(exc: type[Exception], /) -> None: ... # We want to be able to generate docs without module @@ -288,9 +290,25 @@ class SdBusLibraryError(SdBusBaseError): ... -DBUS_ERROR_TO_EXCEPTION: Dict[str, Exception] = {} +class SdBusRequestNameError(SdBusBaseError): + ... -EXCEPTION_TO_DBUS_ERROR: Dict[Exception, str] = {} + +class SdBusRequestNameInQueueError(SdBusRequestNameError): + ... + + +class SdBusRequestNameExistsError(SdBusRequestNameError): + ... + + +class SdBusRequestNameAlreadyOwnerError(SdBusRequestNameError): + ... + + +DBUS_ERROR_TO_EXCEPTION: dict[str, type[Exception]] = {} + +EXCEPTION_TO_DBUS_ERROR: dict[type[Exception], str] = {} DbusDeprecatedFlag: int = 0 DbusHiddenFlag: int = 0 @@ -301,3 +319,7 @@ class SdBusLibraryError(SdBusBaseError): DbusPropertyEmitsInvalidationFlag: int = 0 DbusPropertyExplicitFlag: int = 0 DbusSensitiveFlag: int = 0 + +NameAllowReplacementFlag: int = 0 +NameReplaceExistingFlag: int = 0 +NameQueueFlag: int = 0 diff --git a/src/sdbus/sd_bus_internals_bus.c b/src/sdbus/sd_bus_internals_bus.c index fb920b4..ca7b817 100644 --- a/src/sdbus/sd_bus_internals_bus.c +++ b/src/sdbus/sd_bus_internals_bus.c @@ -19,11 +19,24 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #include +#include +#include +#include #include "sd_bus_internals.h" static void SdBus_dealloc(SdBusObject* self) { + if (NULL != self->loop && NULL != self->bus_fd) { + Py_XDECREF(PyObject_CallMethodObjArgs(self->loop, remove_reader_str, self->bus_fd, NULL)); + Py_XDECREF(PyObject_CallMethodObjArgs(self->loop, remove_writer_str, self->bus_fd, NULL)); + } + if (NULL != self->timer_fd) { + Py_XDECREF(PyObject_CallMethodObjArgs(self->loop, remove_reader_str, self->timer_fd, NULL)); + Py_DECREF(self->timer_fd); + close(self->timer_fd_int); + } sd_bus_unref(self->sd_bus_ref); - Py_XDECREF(self->reader_fd); + Py_XDECREF(self->bus_fd); + Py_XDECREF(self->loop); SD_BUS_DEALLOC_TAIL; } @@ -155,22 +168,10 @@ static SdBusMessageObject* SdBus_new_signal_message(SdBusObject* self, PyObject* return new_message_object; } -#ifndef Py_LIMITED_API -static int _check_sdbus_message(PyObject* something) { - return PyType_IsSubtype(Py_TYPE(something), (PyTypeObject*)SdBusMessage_class); -} - -static SdBusMessageObject* SdBus_call(SdBusObject* self, PyObject* const* args, Py_ssize_t nargs) { - // TODO: Check reference counting - SD_BUS_PY_CHECK_ARGS_NUMBER(1); - SD_BUS_PY_CHECK_ARG_CHECK_FUNC(0, _check_sdbus_message); - - SdBusMessageObject* call_message = (SdBusMessageObject*)args[0]; -#else -static SdBusMessageObject* SdBus_call(SdBusObject* self, PyObject* args) { +static SdBusMessageObject* SdBus_call(SdBusObject* self, PyObject* arg) { SdBusMessageObject* call_message = NULL; - CALL_PYTHON_BOOL_CHECK(PyArg_ParseTuple(args, "O", &call_message, NULL)); -#endif + CALL_PYTHON_BOOL_CHECK(PyArg_Parse(arg, "O!", SdBusMessage_class, &call_message, NULL)); + SdBusMessageObject* reply_message_object CLEANUP_SD_BUS_MESSAGE = (SdBusMessageObject*)CALL_PYTHON_AND_CHECK(SD_BUS_PY_CLASS_DUNDER_NEW(SdBusMessage_class)); @@ -229,46 +230,31 @@ int future_set_exception_from_message(PyObject* future, sd_bus_message* message) return 0; } -static PyObject* SdBus_drive(SdBusObject* self, PyObject* Py_UNUSED(args)); - static PyObject* SdBus_get_fd(SdBusObject* self, PyObject* Py_UNUSED(args)) { int file_descriptor = CALL_SD_BUS_AND_CHECK(sd_bus_get_fd(self->sd_bus_ref)); return PyLong_FromLong((long)file_descriptor); } -#define CHECK_SD_BUS_READER \ - ({ \ - if (self->reader_fd == NULL) { \ - CALL_PYTHON_EXPECT_NONE(register_reader(self)); \ - } \ - }) - -PyObject* register_reader(SdBusObject* self) { - PyObject* running_loop CLEANUP_PY_OBJECT = CALL_PYTHON_AND_CHECK(PyObject_CallFunctionObjArgs(asyncio_get_running_loop, NULL)); - PyObject* new_reader_fd CLEANUP_PY_OBJECT = CALL_PYTHON_AND_CHECK(SdBus_get_fd(self, NULL)); - PyObject* drive_method CLEANUP_PY_OBJECT = CALL_PYTHON_AND_CHECK(PyObject_GetAttrString((PyObject*)self, "drive")); - Py_XDECREF(CALL_PYTHON_AND_CHECK(PyObject_CallMethodObjArgs(running_loop, add_reader_str, new_reader_fd, drive_method, NULL))); - Py_INCREF(new_reader_fd); - self->reader_fd = new_reader_fd; - Py_RETURN_NONE; -} +static PyObject* SdBus_asyncio_update_fd_watchers(SdBusObject* self); -PyObject* unregister_reader(SdBusObject* self) { - PyObject* running_loop CLEANUP_PY_OBJECT = CALL_PYTHON_AND_CHECK(PyObject_CallFunctionObjArgs(asyncio_get_running_loop, NULL)); - Py_XDECREF(CALL_PYTHON_AND_CHECK(PyObject_CallMethodObjArgs(running_loop, remove_reader_str, self->reader_fd, NULL))); - Py_RETURN_NONE; +#define CHECK_ASYNCIO_WATCHERS ({ CALL_PYTHON_EXPECT_NONE(SdBus_asyncio_update_fd_watchers(self)); }) + +static PyObject* _get_or_bind_loop(SdBusObject* self) { + if (NULL == self->loop) { + self->loop = CALL_PYTHON_AND_CHECK(PyObject_CallFunctionObjArgs(asyncio_get_running_loop, NULL)); + } + return self->loop; } -static PyObject* SdBus_drive(SdBusObject* self, PyObject* Py_UNUSED(args)) { +static PyObject* SdBus_process(SdBusObject* self, PyObject* Py_UNUSED(args)) { int return_value = 1; while (return_value > 0) { return_value = sd_bus_process(self->sd_bus_ref, NULL); if (return_value < 0) { - CALL_PYTHON_AND_CHECK(unregister_reader(self)); if (-ECONNRESET == return_value) { // Connection gracefully terminated - Py_RETURN_NONE; + break; } else { // Error occurred processing sdbus CALL_SD_BUS_AND_CHECK(return_value); @@ -280,6 +266,7 @@ static PyObject* SdBus_drive(SdBusObject* self, PyObject* Py_UNUSED(args)) { return NULL; } } + CHECK_ASYNCIO_WATCHERS; Py_RETURN_NONE; } @@ -291,7 +278,7 @@ int SdBus_async_callback(sd_bus_message* m, PyObject* py_future = userdata; PyObject* is_cancelled CLEANUP_PY_OBJECT = PyObject_CallMethod(py_future, "cancelled", ""); if (Py_True == is_cancelled) { - // A bit unpythonic but SdBus_drive does not error out + // A bit unpythonic but SdBus_process does not error out return 0; } @@ -317,18 +304,11 @@ int SdBus_async_callback(sd_bus_message* m, return 0; } -#ifndef Py_LIMITED_API -static PyObject* SdBus_call_async(SdBusObject* self, PyObject* const* args, Py_ssize_t nargs) { - SD_BUS_PY_CHECK_ARGS_NUMBER(1); - SD_BUS_PY_CHECK_ARG_CHECK_FUNC(0, _check_sdbus_message); - - SdBusMessageObject* call_message = (SdBusMessageObject*)args[0]; -#else -static PyObject* SdBus_call_async(SdBusObject* self, PyObject* args) { +static PyObject* SdBus_call_async(SdBusObject* self, PyObject* arg) { SdBusMessageObject* call_message = NULL; - CALL_PYTHON_BOOL_CHECK(PyArg_ParseTuple(args, "O", &call_message, NULL)); -#endif - PyObject* running_loop CLEANUP_PY_OBJECT = CALL_PYTHON_AND_CHECK(PyObject_CallFunctionObjArgs(asyncio_get_running_loop, NULL)); + CALL_PYTHON_BOOL_CHECK(PyArg_Parse(arg, "O!", SdBusMessage_class, &call_message, NULL)); + + PyObject* running_loop = CALL_PYTHON_AND_CHECK(_get_or_bind_loop(self)); PyObject* new_future = CALL_PYTHON_AND_CHECK(PyObject_CallMethod(running_loop, "create_future", "")); @@ -340,7 +320,7 @@ static PyObject* SdBus_call_async(SdBusObject* self, PyObject* args) { if (PyObject_SetAttrString(new_future, "_sd_bus_py_slot", (PyObject*)new_slot_object) < 0) { return NULL; } - CHECK_SD_BUS_READER; + CHECK_ASYNCIO_WATCHERS; return new_future; } @@ -376,17 +356,16 @@ static PyObject* SdBus_add_interface(SdBusObject* self, PyObject* args) { } int _SdBus_signal_callback(sd_bus_message* m, void* userdata, sd_bus_error* Py_UNUSED(ret_error)) { - PyObject* async_queue = userdata; + PyObject* signal_callback = userdata; - SdBusMessageObject* new_message_object CLEANUP_SD_BUS_MESSAGE = (SdBusMessageObject*)SD_BUS_PY_CLASS_DUNDER_NEW(SdBusMessage_class); - if (new_message_object == NULL) { - return -1; - } + PyObject* running_loop CLEANUP_PY_OBJECT = CALL_PYTHON_CHECK_RETURN_NEG1(PyObject_CallFunctionObjArgs(asyncio_get_running_loop, NULL)); + + SdBusMessageObject* new_message_object CLEANUP_SD_BUS_MESSAGE = + (SdBusMessageObject*)CALL_PYTHON_CHECK_RETURN_NEG1(SD_BUS_PY_CLASS_DUNDER_NEW(SdBusMessage_class)); _SdBusMessage_set_messsage(new_message_object, m); - PyObject* should_be_none CLEANUP_PY_OBJECT = PyObject_CallMethodObjArgs(async_queue, put_no_wait_str, new_message_object, NULL); - if (should_be_none == NULL) { - return -1; - } + + Py_XDECREF(CALL_PYTHON_CHECK_RETURN_NEG1(PyObject_CallMethodObjArgs(running_loop, call_soon_str, signal_callback, new_message_object, NULL))); + return 0; } @@ -394,21 +373,15 @@ int _SdBus_match_signal_instant_callback(sd_bus_message* m, void* userdata, sd_b PyObject* new_future = userdata; if (!sd_bus_message_is_method_error(m, NULL)) { - PyObject* new_queue CLEANUP_PY_OBJECT = PyObject_GetAttrString(new_future, "_sd_bus_queue"); - if (new_queue == NULL) { - return -1; - } + SdBusSlotObject* slot_object CLEANUP_SD_BUS_SLOT = + (SdBusSlotObject*)CALL_PYTHON_CHECK_RETURN_NEG1(PyObject_GetAttrString(new_future, "_sd_bus_slot")); - PyObject* should_be_none CLEANUP_PY_OBJECT = PyObject_CallMethodObjArgs(new_future, set_result_str, new_queue, NULL); - if (should_be_none == NULL) { - return -1; - } + Py_XDECREF(CALL_PYTHON_CHECK_RETURN_NEG1(PyObject_CallMethodObjArgs(new_future, set_result_str, slot_object, NULL))); - SdBusSlotObject* slot_object CLEANUP_SD_BUS_SLOT = (SdBusSlotObject*)PyObject_GetAttrString(new_queue, "_sd_bus_slot"); - if (slot_object == NULL) { - return -1; - } - sd_bus_slot_set_userdata(slot_object->slot_ref, new_queue); + PyObject* signal_callback = CALL_PYTHON_CHECK_RETURN_NEG1(PyObject_GetAttrString(new_future, "_sd_bus_signal_callback")); + + sd_bus_slot_set_userdata(slot_object->slot_ref, signal_callback); + sd_bus_slot_set_destroy_callback(slot_object->slot_ref, (sd_bus_destroy_t)Py_DecRef); } else { if (future_set_exception_from_message(new_future, m) < 0) { return -1; @@ -424,66 +397,84 @@ static int _unicode_or_none(PyObject* some_object) { return (PyUnicode_Check(some_object) || (Py_None == some_object)); } -static PyObject* SdBus_get_signal_queue(SdBusObject* self, PyObject* const* args, Py_ssize_t nargs) { - SD_BUS_PY_CHECK_ARGS_NUMBER(4); +static PyObject* SdBus_match_signal_async(SdBusObject* self, PyObject* const* args, Py_ssize_t nargs) { + SD_BUS_PY_CHECK_ARGS_NUMBER(5); SD_BUS_PY_CHECK_ARG_CHECK_FUNC(0, _unicode_or_none); SD_BUS_PY_CHECK_ARG_CHECK_FUNC(1, _unicode_or_none); SD_BUS_PY_CHECK_ARG_CHECK_FUNC(2, _unicode_or_none); SD_BUS_PY_CHECK_ARG_CHECK_FUNC(3, _unicode_or_none); + SD_BUS_PY_CHECK_ARG_CHECK_FUNC(4, PyCallable_Check); const char* sender_service_char_ptr = SD_BUS_PY_UNICODE_AS_CHAR_PTR_OPTIONAL(args[0]); const char* path_name_char_ptr = SD_BUS_PY_UNICODE_AS_CHAR_PTR_OPTIONAL(args[1]); const char* interface_name_char_ptr = SD_BUS_PY_UNICODE_AS_CHAR_PTR_OPTIONAL(args[2]); const char* member_name_char_ptr = SD_BUS_PY_UNICODE_AS_CHAR_PTR_OPTIONAL(args[3]); + PyObject* signal_callback = args[4]; #else -static PyObject* SdBus_get_signal_queue(SdBusObject* self, PyObject* args) { +static PyObject* SdBus_match_signal_async(SdBusObject* self, PyObject* args) { const char* sender_service_char_ptr = NULL; const char* path_name_char_ptr = NULL; const char* interface_name_char_ptr = NULL; const char* member_name_char_ptr = NULL; - CALL_PYTHON_BOOL_CHECK( - PyArg_ParseTuple(args, "zzzz", &sender_service_char_ptr, &path_name_char_ptr, &interface_name_char_ptr, &member_name_char_ptr, NULL)); + PyObject* signal_callback = NULL; + CALL_PYTHON_BOOL_CHECK(PyArg_ParseTuple(args, "zzzzO", &sender_service_char_ptr, &path_name_char_ptr, &interface_name_char_ptr, &member_name_char_ptr, + &signal_callback, NULL)); #endif - SdBusSlotObject* new_slot CLEANUP_SD_BUS_SLOT = (SdBusSlotObject*)CALL_PYTHON_AND_CHECK(SD_BUS_PY_CLASS_DUNDER_NEW(SdBusSlot_class)); - - PyObject* new_queue CLEANUP_PY_OBJECT = CALL_PYTHON_AND_CHECK(PyObject_CallFunctionObjArgs(asyncio_queue_class, NULL)); - - // Bind lifetime of the slot to the queue - CALL_PYTHON_INT_CHECK(PyObject_SetAttrString(new_queue, "_sd_bus_slot", (PyObject*)new_slot)); - - PyObject* running_loop CLEANUP_PY_OBJECT = CALL_PYTHON_AND_CHECK(PyObject_CallFunctionObjArgs(asyncio_get_running_loop, NULL)); - + PyObject* running_loop = CALL_PYTHON_AND_CHECK(_get_or_bind_loop(self)); PyObject* new_future CLEANUP_PY_OBJECT = CALL_PYTHON_AND_CHECK(PyObject_CallMethod(running_loop, "create_future", "")); - // Bind lifetime of the queue to future - CALL_PYTHON_INT_CHECK(PyObject_SetAttrString(new_future, "_sd_bus_queue", new_queue)); + SdBusSlotObject* new_slot CLEANUP_SD_BUS_SLOT = (SdBusSlotObject*)CALL_PYTHON_AND_CHECK(SD_BUS_PY_CLASS_DUNDER_NEW(SdBusSlot_class)); + + // Bind lifetime of the slot to the Future + CALL_PYTHON_INT_CHECK(PyObject_SetAttrString(new_future, "_sd_bus_slot", (PyObject*)new_slot)); + CALL_PYTHON_INT_CHECK(PyObject_SetAttrString(new_future, "_sd_bus_signal_callback", signal_callback)); CALL_SD_BUS_AND_CHECK(sd_bus_match_signal_async(self->sd_bus_ref, &new_slot->slot_ref, sender_service_char_ptr, path_name_char_ptr, interface_name_char_ptr, member_name_char_ptr, _SdBus_signal_callback, _SdBus_match_signal_instant_callback, new_future)); - CHECK_SD_BUS_READER; + CHECK_ASYNCIO_WATCHERS; Py_INCREF(new_future); return new_future; } -int SdBus_request_callback(sd_bus_message* m, - void* userdata, // Should be the asyncio.Future - sd_bus_error* Py_UNUSED(ret_error)) { +int SdBus_request_name_callback(sd_bus_message* m, + void* userdata, // Should be the asyncio.Future + sd_bus_error* Py_UNUSED(ret_error)) { PyObject* py_future = userdata; PyObject* is_cancelled CLEANUP_PY_OBJECT = PyObject_CallMethod(py_future, "cancelled", ""); if (Py_True == is_cancelled) { - // A bit unpythonic but SdBus_drive does not error out + // A bit unpythonic but SdBus_process does not error out return 0; } if (!sd_bus_message_is_method_error(m, NULL)) { - // Not Error, set Future result to new message object - PyObject* return_object CLEANUP_PY_OBJECT = PyObject_CallMethod(py_future, "set_result", "O", Py_None); - if (return_object == NULL) { - return -1; + uint32_t request_name_result = 0; + CALL_SD_BUS_CHECK_RETURN_NEG1(sd_bus_message_read_basic(m, 'u', &request_name_result)); + if (1 == request_name_result) { + // Successfully acquired the name + Py_XDECREF(CALL_PYTHON_CHECK_RETURN_NEG1(PyObject_CallMethod(py_future, "set_result", "O", Py_None))); + return 0; + } + + PyObject* exception_to_raise CLEANUP_PY_OBJECT = NULL; + switch (request_name_result) { + case 2: + exception_to_raise = CALL_PYTHON_CHECK_RETURN_NEG1(PyObject_CallFunctionObjArgs(exception_request_name_in_queue, NULL)); + break; + case 3: + exception_to_raise = CALL_PYTHON_CHECK_RETURN_NEG1(PyObject_CallFunctionObjArgs(exception_request_name_exists, NULL)); + break; + case 4: + exception_to_raise = CALL_PYTHON_CHECK_RETURN_NEG1(PyObject_CallFunctionObjArgs(exception_request_name_already_owner, NULL)); + break; + default: + exception_to_raise = CALL_PYTHON_CHECK_RETURN_NEG1(PyObject_CallFunctionObjArgs(exception_request_name, NULL)); + break; } + Py_XDECREF(CALL_PYTHON_CHECK_RETURN_NEG1(PyObject_CallMethod(py_future, "set_exception", "O", exception_to_raise))); + return -1; } else { // An Error, set exception if (future_set_exception_from_message(py_future, m) < 0) { @@ -512,17 +503,15 @@ static PyObject* SdBus_request_name_async(SdBusObject* self, PyObject* args) { CALL_PYTHON_BOOL_CHECK(PyArg_ParseTuple(args, "sK", &service_name_char_ptr, &flags_long_long, NULL)); uint64_t flags = (uint64_t)flags_long_long; #endif - PyObject* running_loop CLEANUP_PY_OBJECT = CALL_PYTHON_AND_CHECK(PyObject_CallFunctionObjArgs(asyncio_get_running_loop, NULL)); + PyObject* running_loop = CALL_PYTHON_AND_CHECK(_get_or_bind_loop(self)); PyObject* new_future = CALL_PYTHON_AND_CHECK(PyObject_CallMethod(running_loop, "create_future", "")); SdBusSlotObject* new_slot_object CLEANUP_SD_BUS_SLOT = (SdBusSlotObject*)CALL_PYTHON_AND_CHECK(SD_BUS_PY_CLASS_DUNDER_NEW(SdBusSlot_class)); CALL_SD_BUS_AND_CHECK( - sd_bus_request_name_async(self->sd_bus_ref, &new_slot_object->slot_ref, service_name_char_ptr, flags, SdBus_request_callback, new_future)); + sd_bus_request_name_async(self->sd_bus_ref, &new_slot_object->slot_ref, service_name_char_ptr, flags, SdBus_request_name_callback, new_future)); - if (PyObject_SetAttrString(new_future, "_sd_bus_py_slot", (PyObject*)new_slot_object) < 0) { - return NULL; - } - CHECK_SD_BUS_READER; + CALL_PYTHON_INT_CHECK(PyObject_SetAttrString(new_future, "_sd_bus_py_slot", (PyObject*)new_slot_object)); + CHECK_ASYNCIO_WATCHERS; return new_future; } @@ -544,8 +533,25 @@ static PyObject* SdBus_request_name(SdBusObject* self, PyObject* args) { CALL_PYTHON_BOOL_CHECK(PyArg_ParseTuple(args, "sK", &service_name_char_ptr, &flags_long_long, NULL)); uint64_t flags = (uint64_t)flags_long_long; #endif - CALL_SD_BUS_AND_CHECK(sd_bus_request_name(self->sd_bus_ref, service_name_char_ptr, flags)); - Py_RETURN_NONE; + int request_name_return_code = sd_bus_request_name(self->sd_bus_ref, service_name_char_ptr, flags); + switch (request_name_return_code) { + case -EEXIST: + return PyErr_Format(exception_request_name_exists, "Name \"%s\" already owned.", service_name_char_ptr, NULL); + break; + case -EALREADY: + return PyErr_Format(exception_request_name_already_owner, "Already own name \"%s\".", service_name_char_ptr, NULL); + break; + case 0: + return PyErr_Format(exception_request_name_in_queue, "Queued up to acquire name \"%s\".", service_name_char_ptr, NULL); + break; + case 1: + Py_RETURN_NONE; + break; + default: + CALL_SD_BUS_AND_CHECK(request_name_return_code); + break; + } + Py_UNREACHABLE(); } #ifndef Py_LIMITED_API @@ -601,6 +607,14 @@ static PyObject* SdBus_emit_object_removed(SdBusObject* self, PyObject* args) { static PyObject* SdBus_close(SdBusObject* self, PyObject* Py_UNUSED(args)) { sd_bus_close(self->sd_bus_ref); + if (NULL != self->loop && NULL != self->bus_fd) { + Py_XDECREF(CALL_PYTHON_AND_CHECK(PyObject_CallMethodObjArgs(self->loop, remove_reader_str, self->bus_fd, NULL))); + Py_XDECREF(CALL_PYTHON_AND_CHECK(PyObject_CallMethodObjArgs(self->loop, remove_writer_str, self->bus_fd, NULL))); + } + if (NULL != self->timer_fd) { + Py_XDECREF(PyObject_CallMethodObjArgs(self->loop, remove_reader_str, self->timer_fd, NULL)); + // TODO: Close timerfd + } Py_RETURN_NONE; } @@ -609,28 +623,99 @@ static PyObject* SdBus_start(SdBusObject* self, PyObject* Py_UNUSED(args)) { Py_RETURN_NONE; } +static inline int sd_bus_get_events_zero_on_closed(SdBusObject* self) { + int events = sd_bus_get_events(self->sd_bus_ref); + if (-ENOTCONN == events) { + return 0; + } + return events; +}; + +static inline int sd_bus_get_timeout_uint_max_on_closed(SdBusObject* self, uint64_t* timeout_usec) { + int r = sd_bus_get_timeout(self->sd_bus_ref, timeout_usec); + if (-ENOTCONN == r) { + *timeout_usec = UINT64_MAX; + return 0; + } + return r; +} + +static PyObject* SdBus_asyncio_update_fd_watchers(SdBusObject* self) { + PyObject* running_loop = CALL_PYTHON_AND_CHECK(_get_or_bind_loop(self)); + PyObject* drive_method CLEANUP_PY_OBJECT = CALL_PYTHON_AND_CHECK(PyObject_GetAttrString((PyObject*)self, "process")); + + if (NULL == self->timer_fd) { + self->timer_fd_int = CALL_SD_BUS_AND_CHECK(timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK | TFD_CLOEXEC)); + if (self->timer_fd_int < 0) { + PyErr_SetFromErrno(PyExc_OSError); + } + PyObject* timer_fd CLEANUP_PY_OBJECT = PyLong_FromLong((int)self->timer_fd_int); + Py_XDECREF(CALL_PYTHON_AND_CHECK(PyObject_CallMethodObjArgs(running_loop, add_reader_str, timer_fd, drive_method, NULL))); + Py_INCREF(timer_fd); + self->timer_fd = timer_fd; + } + + uint64_t timeout_usec = UINT64_MAX; + CALL_SD_BUS_AND_CHECK(sd_bus_get_timeout_uint_max_on_closed(self, &timeout_usec)); + + struct itimerspec bus_timer = {0}; + if (timeout_usec == UINT64_MAX) { + // Setting bus_timer to zero disarms timer. + } else if (timeout_usec != 0) { + bus_timer.it_value.tv_sec = timeout_usec / 1000000; + bus_timer.it_value.tv_nsec = (timeout_usec % 1000000) * 1000; + } else if (timeout_usec == 0) { + Py_XDECREF(CALL_PYTHON_AND_CHECK(PyObject_CallMethodObjArgs(running_loop, call_soon_str, drive_method, NULL))); + } + + CALL_SD_BUS_AND_CHECK(timerfd_settime(self->timer_fd_int, TFD_TIMER_ABSTIME, &bus_timer, NULL)); + + int events_to_watch = CALL_SD_BUS_AND_CHECK(sd_bus_get_events_zero_on_closed(self)); + if (events_to_watch == self->asyncio_watchers_last_state) { + // Do not update the watchers because state is the same + Py_RETURN_NONE; + } else { + self->asyncio_watchers_last_state = events_to_watch; + } + + if (NULL == self->bus_fd) { + self->bus_fd = CALL_PYTHON_AND_CHECK(SdBus_get_fd(self, NULL)); + } + + if (events_to_watch & POLLIN) { + Py_XDECREF(CALL_PYTHON_AND_CHECK(PyObject_CallMethodObjArgs(running_loop, add_reader_str, self->bus_fd, drive_method, NULL))); + } else { + Py_XDECREF(CALL_PYTHON_AND_CHECK(PyObject_CallMethodObjArgs(running_loop, remove_reader_str, self->bus_fd, NULL))); + } + + if (events_to_watch & POLLOUT) { + Py_XDECREF(CALL_PYTHON_AND_CHECK(PyObject_CallMethodObjArgs(running_loop, add_writer_str, self->bus_fd, drive_method, NULL))); + } else { + Py_XDECREF(CALL_PYTHON_AND_CHECK(PyObject_CallMethodObjArgs(running_loop, remove_writer_str, self->bus_fd, NULL))); + } + + Py_RETURN_NONE; +} + static PyMethodDef SdBus_methods[] = { - {"call", (SD_BUS_PY_FUNC_TYPE)SdBus_call, SD_BUS_PY_METH, "Send message and get reply"}, - {"call_async", (SD_BUS_PY_FUNC_TYPE)SdBus_call_async, SD_BUS_PY_METH, "Async send message, returns awaitable future"}, - {"drive", (PyCFunction)SdBus_drive, METH_NOARGS, "Drive connection"}, - {"get_fd", (SD_BUS_PY_FUNC_TYPE)SdBus_get_fd, SD_BUS_PY_METH, "Get file descriptor to await on"}, - {"new_method_call_message", (SD_BUS_PY_FUNC_TYPE)SdBus_new_method_call_message, SD_BUS_PY_METH, NULL}, - {"new_property_get_message", (SD_BUS_PY_FUNC_TYPE)SdBus_new_property_get_message, SD_BUS_PY_METH, NULL}, - {"new_property_set_message", (SD_BUS_PY_FUNC_TYPE)SdBus_new_property_set_message, SD_BUS_PY_METH, - "Set object/interface property. User must add variant data to " - "message"}, - {"new_signal_message", (SD_BUS_PY_FUNC_TYPE)SdBus_new_signal_message, SD_BUS_PY_METH, "Create new signal message. User must data to message and send it"}, - {"add_interface", (SD_BUS_PY_FUNC_TYPE)SdBus_add_interface, SD_BUS_PY_METH, "Add interface to the bus"}, - {"get_signal_queue_async", (SD_BUS_PY_FUNC_TYPE)SdBus_get_signal_queue, SD_BUS_PY_METH, - "Returns a future that returns a queue that queues signal " - "messages"}, - {"request_name_async", (SD_BUS_PY_FUNC_TYPE)SdBus_request_name_async, SD_BUS_PY_METH, "Request dbus name async"}, - {"request_name", (SD_BUS_PY_FUNC_TYPE)SdBus_request_name, SD_BUS_PY_METH, "Request dbus name blocking"}, - {"add_object_manager", (SD_BUS_PY_FUNC_TYPE)SdBus_add_object_manager, SD_BUS_PY_METH, "Add object manager at the path"}, - {"emit_object_added", (SD_BUS_PY_FUNC_TYPE)SdBus_emit_object_added, SD_BUS_PY_METH, "Emit signal that object was added"}, - {"emit_object_removed", (SD_BUS_PY_FUNC_TYPE)SdBus_emit_object_removed, SD_BUS_PY_METH, "Emit signal that object was removed"}, - {"close", (PyCFunction)SdBus_close, METH_NOARGS, "Close connection"}, - {"start", (PyCFunction)SdBus_start, METH_NOARGS, "Start connection"}, + {"call", (PyCFunction)SdBus_call, METH_O, PyDoc_STR("Send message and block until the reply.")}, + {"call_async", (PyCFunction)SdBus_call_async, METH_O, PyDoc_STR("Async send message, returns awaitable future.")}, + {"process", (PyCFunction)SdBus_process, METH_NOARGS, PyDoc_STR("Process pending IO work.")}, + {"get_fd", (SD_BUS_PY_FUNC_TYPE)SdBus_get_fd, SD_BUS_PY_METH, PyDoc_STR("Get file descriptor to poll on.")}, + {"new_method_call_message", (SD_BUS_PY_FUNC_TYPE)SdBus_new_method_call_message, SD_BUS_PY_METH, PyDoc_STR("Create new empty method call message.")}, + {"new_property_get_message", (SD_BUS_PY_FUNC_TYPE)SdBus_new_property_get_message, SD_BUS_PY_METH, PyDoc_STR("Create new empty property get message.")}, + {"new_property_set_message", (SD_BUS_PY_FUNC_TYPE)SdBus_new_property_set_message, SD_BUS_PY_METH, PyDoc_STR("Create new empty property set message.")}, + {"new_signal_message", (SD_BUS_PY_FUNC_TYPE)SdBus_new_signal_message, SD_BUS_PY_METH, PyDoc_STR("Create new empty signal message.")}, + {"add_interface", (SD_BUS_PY_FUNC_TYPE)SdBus_add_interface, SD_BUS_PY_METH, PyDoc_STR("Add interface to the bus.")}, + {"match_signal_async", (SD_BUS_PY_FUNC_TYPE)SdBus_match_signal_async, SD_BUS_PY_METH, + PyDoc_STR("Register signal callback asynchronously. Returns a Future that returns a SdBusSlot.")}, + {"request_name_async", (SD_BUS_PY_FUNC_TYPE)SdBus_request_name_async, SD_BUS_PY_METH, PyDoc_STR("Request D-Bus name async.")}, + {"request_name", (SD_BUS_PY_FUNC_TYPE)SdBus_request_name, SD_BUS_PY_METH, PyDoc_STR("Request D-Bus name blocking.")}, + {"add_object_manager", (SD_BUS_PY_FUNC_TYPE)SdBus_add_object_manager, SD_BUS_PY_METH, PyDoc_STR("Add object manager at the path.")}, + {"emit_object_added", (SD_BUS_PY_FUNC_TYPE)SdBus_emit_object_added, SD_BUS_PY_METH, PyDoc_STR("Emit signal that object was added.")}, + {"emit_object_removed", (SD_BUS_PY_FUNC_TYPE)SdBus_emit_object_removed, SD_BUS_PY_METH, PyDoc_STR("Emit signal that object was removed.")}, + {"close", (PyCFunction)SdBus_close, METH_NOARGS, PyDoc_STR("Close connection.")}, + {"start", (PyCFunction)SdBus_start, METH_NOARGS, PyDoc_STR("Start connection.")}, {NULL, NULL, 0, NULL}, }; @@ -646,8 +731,31 @@ static PyObject* SdBus_address_getter(SdBusObject* self, void* Py_UNUSED(closure return PyUnicode_FromString(bus_address); } +static PyObject* SdBus_method_call_timeout_usec_getter(SdBusObject* self, void* Py_UNUSED(closure)) { + uint64_t timeout_usec = 0; + CALL_SD_BUS_AND_CHECK(sd_bus_get_method_call_timeout(self->sd_bus_ref, &timeout_usec)); + + return PyLong_FromUnsignedLongLong((unsigned long long)timeout_usec); +} + +static int SdBus_method_call_timeout_usec_setter(SdBusObject* self, PyObject* new_value, void* Py_UNUSED(closure)) { + if (NULL == new_value) { + PyErr_SetString(PyExc_ValueError, "Cannot delete method call timeout value"); + return -1; + } + + unsigned long long new_timeout_usec = PyLong_AsUnsignedLongLong(new_value); + if ((((unsigned long long)-1) == new_timeout_usec) && (PyErr_Occurred() != NULL)) { + return -1; + } + CALL_SD_BUS_CHECK_RETURN_NEG1(sd_bus_set_method_call_timeout(self->sd_bus_ref, (uint64_t)new_timeout_usec)); + return 0; +} + static PyGetSetDef SdBus_properies[] = { - {"address", (getter)SdBus_address_getter, NULL, "Bus address", NULL}, + {"address", (getter)SdBus_address_getter, NULL, PyDoc_STR("Bus address."), NULL}, + {"method_call_timeout_usec", (getter)SdBus_method_call_timeout_usec_getter, (setter)SdBus_method_call_timeout_usec_setter, + PyDoc_STR("D-Bus call timeout in microseconds."), NULL}, {0}, }; diff --git a/src/sdbus/sd_bus_internals_funcs.c b/src/sdbus/sd_bus_internals_funcs.c index 0dbee31..b83e621 100644 --- a/src/sdbus/sd_bus_internals_funcs.c +++ b/src/sdbus/sd_bus_internals_funcs.c @@ -144,7 +144,7 @@ static PyObject* map_exception_to_dbus_error(PyObject* Py_UNUSED(self), PyObject #endif if (CALL_PYTHON_INT_CHECK(PyDict_Contains(dbus_error_to_exception_dict, dbus_error_string)) > 0) { - PyErr_Format(PyExc_ValueError, "Dbus error %R is already mapped.", dbus_error_string); + PyErr_Format(PyExc_ValueError, "D-Bus error %R is already mapped.", dbus_error_string); return NULL; } @@ -166,7 +166,7 @@ static PyObject* add_exception_mapping(PyObject* Py_UNUSED(self), PyObject* args PyObject* dbus_error_string CLEANUP_PY_OBJECT = CALL_PYTHON_AND_CHECK(PyObject_GetAttrString(exception, "dbus_error_name")); if (CALL_PYTHON_INT_CHECK(PyDict_Contains(dbus_error_to_exception_dict, dbus_error_string)) > 0) { - PyErr_Format(PyExc_ValueError, "Dbus error %R is already mapped.", dbus_error_string); + PyErr_Format(PyExc_ValueError, "D-Bus error %R is already mapped.", dbus_error_string); return NULL; } @@ -274,21 +274,21 @@ static PyObject* is_object_path_valid(PyObject* Py_UNUSED(self), PyObject* args) } PyMethodDef SdBusPyInternal_methods[] = { - {"sd_bus_open", (PyCFunction)sd_bus_py_open, METH_NOARGS, - "Open dbus connection. Session bus running as user or system bus as " - "daemon"}, - {"sd_bus_open_user", (PyCFunction)sd_bus_py_open_user, METH_NOARGS, "Open user session dbus"}, - {"sd_bus_open_system", (PyCFunction)sd_bus_py_open_system, METH_NOARGS, "Open system dbus"}, - {"sd_bus_open_system_remote", (PyCFunction)sd_bus_py_open_system_remote, METH_VARARGS, "Open remote system bus over SSH"}, - {"sd_bus_open_user_machine", (PyCFunction)sd_bus_py_open_user_machine, METH_VARARGS, "Open system bus in systemd-nspawn container"}, - {"sd_bus_open_system_machine", (PyCFunction)sd_bus_py_open_system_machine, METH_VARARGS, "Open user bus in systemd-nspawn container"}, - {"encode_object_path", (SD_BUS_PY_FUNC_TYPE)encode_object_path, SD_BUS_PY_METH, "Encode object path with object path prefix and arbitrary string"}, - {"decode_object_path", (SD_BUS_PY_FUNC_TYPE)decode_object_path, SD_BUS_PY_METH, "Decode object path with object path prefix and arbitrary string"}, - {"map_exception_to_dbus_error", (SD_BUS_PY_FUNC_TYPE)map_exception_to_dbus_error, SD_BUS_PY_METH, "Map exception to a D-Bus error name"}, - {"add_exception_mapping", (SD_BUS_PY_FUNC_TYPE)add_exception_mapping, SD_BUS_PY_METH, "Add exception to the mapping of dbus error names"}, - {"is_interface_name_valid", (SD_BUS_PY_FUNC_TYPE)is_interface_name_valid, SD_BUS_PY_METH, "Is the string valid interface name?"}, - {"is_service_name_valid", (SD_BUS_PY_FUNC_TYPE)is_service_name_valid, SD_BUS_PY_METH, "Is the string valid service name?"}, - {"is_member_name_valid", (SD_BUS_PY_FUNC_TYPE)is_member_name_valid, SD_BUS_PY_METH, "Is the string valid member name?"}, - {"is_object_path_valid", (SD_BUS_PY_FUNC_TYPE)is_object_path_valid, SD_BUS_PY_METH, "Is the string valid object path?"}, + {"sd_bus_open", (PyCFunction)sd_bus_py_open, METH_NOARGS, PyDoc_STR("Open dbus connection. Session bus running as user or system bus as daemon.")}, + {"sd_bus_open_user", (PyCFunction)sd_bus_py_open_user, METH_NOARGS, PyDoc_STR("Open user session dbus.")}, + {"sd_bus_open_system", (PyCFunction)sd_bus_py_open_system, METH_NOARGS, PyDoc_STR("Open system dbus.")}, + {"sd_bus_open_system_remote", (PyCFunction)sd_bus_py_open_system_remote, METH_VARARGS, PyDoc_STR("Open remote system bus over SSH.")}, + {"sd_bus_open_user_machine", (PyCFunction)sd_bus_py_open_user_machine, METH_VARARGS, PyDoc_STR("Open system bus in systemd-nspawn container.")}, + {"sd_bus_open_system_machine", (PyCFunction)sd_bus_py_open_system_machine, METH_VARARGS, PyDoc_STR("Open user bus in systemd-nspawn container.")}, + {"encode_object_path", (SD_BUS_PY_FUNC_TYPE)encode_object_path, SD_BUS_PY_METH, + PyDoc_STR("Encode object path with object path prefix and arbitrary string.")}, + {"decode_object_path", (SD_BUS_PY_FUNC_TYPE)decode_object_path, SD_BUS_PY_METH, + PyDoc_STR("Decode object path with object path prefix and arbitrary string.")}, + {"map_exception_to_dbus_error", (SD_BUS_PY_FUNC_TYPE)map_exception_to_dbus_error, SD_BUS_PY_METH, PyDoc_STR("Map exception to a D-Bus error name.")}, + {"add_exception_mapping", (SD_BUS_PY_FUNC_TYPE)add_exception_mapping, SD_BUS_PY_METH, PyDoc_STR("Add exception to the mapping of dbus error names.")}, + {"is_interface_name_valid", (SD_BUS_PY_FUNC_TYPE)is_interface_name_valid, SD_BUS_PY_METH, PyDoc_STR("Is the string valid interface name?")}, + {"is_service_name_valid", (SD_BUS_PY_FUNC_TYPE)is_service_name_valid, SD_BUS_PY_METH, PyDoc_STR("Is the string valid service name?")}, + {"is_member_name_valid", (SD_BUS_PY_FUNC_TYPE)is_member_name_valid, SD_BUS_PY_METH, PyDoc_STR("Is the string valid member name?")}, + {"is_object_path_valid", (SD_BUS_PY_FUNC_TYPE)is_object_path_valid, SD_BUS_PY_METH, PyDoc_STR("Is the string valid object path?")}, {NULL, NULL, 0, NULL}, }; diff --git a/src/sdbus/sd_bus_internals_interface.c b/src/sdbus/sd_bus_internals_interface.c index 5e518f1..df31e7c 100644 --- a/src/sdbus/sd_bus_internals_interface.c +++ b/src/sdbus/sd_bus_internals_interface.c @@ -224,10 +224,11 @@ static PyObject* SdBusInterface_create_vtable(SdBusInterfaceObject* self, PyObje self->vtable[0] = start_vtable; Py_ssize_t current_index = 1; // Iter method definitions - for (Py_ssize_t i = 0; i < num_of_methods; ({ - ++i; - ++current_index; - })) { + for (Py_ssize_t i = 0; i < num_of_methods; ( + { + ++i; + ++current_index; + })) { PyObject* method_tuple = CALL_PYTHON_AND_CHECK(PyList_GetItem(self->method_list, i)); PyObject* method_name_object = CALL_PYTHON_AND_CHECK(PyTuple_GetItem(method_tuple, 0)); @@ -251,10 +252,11 @@ static PyObject* SdBusInterface_create_vtable(SdBusInterfaceObject* self, PyObje self->vtable[current_index] = temp_vtable; } - for (Py_ssize_t i = 0; i < num_of_properties; ({ - ++i; - ++current_index; - })) { + for (Py_ssize_t i = 0; i < num_of_properties; ( + { + ++i; + ++current_index; + })) { PyObject* property_tuple = SD_BUS_PY_LIST_GET_ITEM(self->property_list, i); PyObject* property_name_str = SD_BUS_PY_TUPLE_GET_ITEM(property_tuple, 0); @@ -290,10 +292,11 @@ static PyObject* SdBusInterface_create_vtable(SdBusInterfaceObject* self, PyObje } } - for (Py_ssize_t i = 0; i < num_of_signals; ({ - ++i; - ++current_index; - })) { + for (Py_ssize_t i = 0; i < num_of_signals; ( + { + ++i; + ++current_index; + })) { PyObject* signal_tuple = SD_BUS_PY_LIST_GET_ITEM(self->signal_list, i); PyObject* signal_name_str = SD_BUS_PY_TUPLE_GET_ITEM(signal_tuple, 0); @@ -320,11 +323,19 @@ static PyObject* SdBusInterface_create_vtable(SdBusInterfaceObject* self, PyObje Py_RETURN_NONE; } +static PyObject* SdBusInterface_stop_export(SdBusInterfaceObject* self, PyObject* Py_UNUSED(args)) { + Py_XDECREF(self->interface_slot); + self->interface_slot = NULL; + + Py_RETURN_NONE; +} + static PyMethodDef SdBusInterface_methods[] = { - {"add_method", (SD_BUS_PY_FUNC_TYPE)SdBusInterface_add_method, SD_BUS_PY_METH, "Add method to the dbus interface"}, - {"add_property", (SD_BUS_PY_FUNC_TYPE)SdBusInterface_add_property, SD_BUS_PY_METH, "Add property to the dbus interface"}, - {"add_signal", (SD_BUS_PY_FUNC_TYPE)SdBusInterface_add_signal, SD_BUS_PY_METH, "Add signal to the dbus interface"}, - {"_create_vtable", (PyCFunction)SdBusInterface_create_vtable, METH_NOARGS, "Creates the vtable"}, + {"add_method", (SD_BUS_PY_FUNC_TYPE)SdBusInterface_add_method, SD_BUS_PY_METH, PyDoc_STR("Add method to the D-Bus interface.")}, + {"add_property", (SD_BUS_PY_FUNC_TYPE)SdBusInterface_add_property, SD_BUS_PY_METH, PyDoc_STR("Add property to the D-Bus interface.")}, + {"add_signal", (SD_BUS_PY_FUNC_TYPE)SdBusInterface_add_signal, SD_BUS_PY_METH, PyDoc_STR("Add signal to the D-Bus interface.")}, + {"_create_vtable", (PyCFunction)SdBusInterface_create_vtable, METH_NOARGS, PyDoc_STR("Creates the vtable.")}, + {"_stop_export", (PyCFunction)SdBusInterface_stop_export, METH_NOARGS, PyDoc_STR("Stop exporting object.")}, {NULL, NULL, 0, NULL}, }; diff --git a/src/sdbus/sd_bus_internals_message.c b/src/sdbus/sd_bus_internals_message.c index d6f7652..71d9b0e 100644 --- a/src/sdbus/sd_bus_internals_message.c +++ b/src/sdbus/sd_bus_internals_message.c @@ -18,6 +18,7 @@ License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ +#include #include "sd_bus_internals.h" void _SdBusMessage_set_messsage(SdBusMessageObject* self, sd_bus_message* new_message) { @@ -577,7 +578,7 @@ static PyObject* _parse_complete(PyObject* complete_obj, _Parse_state* parser_st } case '{': { // Dict - PyErr_SetString(PyExc_TypeError, "Dbus dict can't be outside of array"); + PyErr_SetString(PyExc_TypeError, "D-Bus dict can't be outside of array"); return NULL; break; } @@ -800,6 +801,10 @@ static PyObject* _iter_basic(sd_bus_message* message, char basic_type) { case 'h': { int new_fd = 0; CALL_SD_BUS_AND_CHECK(sd_bus_message_read_basic(message, basic_type, &new_fd)); + + // The fd is owned by the message and would be closed after the end of the message's lifetime + new_fd = CALL_SD_BUS_AND_CHECK(fcntl(new_fd, F_DUPFD_CLOEXEC, 3)); + return PyLong_FromLong((long)new_fd); break; } @@ -966,6 +971,7 @@ static PyObject* SdBusMessage_get_contents2(SdBusMessageObject* self, PyObject* Py_RETURN_NONE; } + CALL_SD_BUS_AND_CHECK(sd_bus_message_rewind(self->message_ref, 0)); _Parse_state read_parser = { .message = self->message_ref, .container_char_ptr = message_signature, @@ -979,6 +985,29 @@ static PyObject* SdBusMessage_get_contents2(SdBusMessageObject* self, PyObject* return iter_tuple_or_single(&read_parser); } +static PyObject* SdBusMessage_parse_to_tuple(SdBusMessageObject* self, PyObject* Py_UNUSED(args)) { + const char* message_signature = sd_bus_message_get_signature(self->message_ref, 0); + + if (message_signature == NULL) { + PyErr_SetString(PyExc_ValueError, "Failed to get message signature."); + return NULL; + } + if (message_signature[0] == '\0') { + // Empty message. Return zero size tuple. + return PyTuple_New(0); + } + + CALL_SD_BUS_AND_CHECK(sd_bus_message_rewind(self->message_ref, 0)); + _Parse_state read_parser = { + .message = self->message_ref, + .container_char_ptr = message_signature, + .index = 0, + .max_index = strlen(message_signature), + }; + + return _iter_struct(&read_parser); +} + #ifndef Py_LIMITED_API static SdBusMessageObject* SdBusMessage_create_error_reply(SdBusMessageObject* self, PyObject* const* args, Py_ssize_t nargs) { SD_BUS_PY_CHECK_ARGS_NUMBER(2); @@ -1003,17 +1032,19 @@ static SdBusMessageObject* SdBusMessage_create_error_reply(SdBusMessageObject* s } static PyMethodDef SdBusMessage_methods[] = { - {"append_data", (SD_BUS_PY_FUNC_TYPE)SdBusMessage_append_data, SD_BUS_PY_METH, "Append basic data based on signature."}, - {"open_container", (SD_BUS_PY_FUNC_TYPE)SdBusMessage_open_container, SD_BUS_PY_METH, "Open container for writing"}, - {"close_container", (PyCFunction)SdBusMessage_close_container, METH_NOARGS, "Close container"}, - {"enter_container", (SD_BUS_PY_FUNC_TYPE)SdBusMessage_enter_container, SD_BUS_PY_METH, "Enter container for reading"}, - {"exit_container", (PyCFunction)SdBusMessage_exit_container, METH_NOARGS, "Exit container"}, - {"dump", (PyCFunction)SdBusMessage_dump, METH_NOARGS, "Dump message to stdout"}, - {"seal", (PyCFunction)SdBusMessage_seal, METH_NOARGS, "Seal message contents"}, - {"get_contents", (PyCFunction)SdBusMessage_get_contents2, METH_NOARGS, "Iterate over message contents"}, - {"create_reply", (PyCFunction)SdBusMessage_create_reply, METH_NOARGS, "Create reply message"}, - {"create_error_reply", (SD_BUS_PY_FUNC_TYPE)SdBusMessage_create_error_reply, SD_BUS_PY_METH, "Create error reply with error name and error message"}, - {"send", (PyCFunction)SdBusMessage_send, METH_NOARGS, "Queue message to be sent"}, + {"append_data", (SD_BUS_PY_FUNC_TYPE)SdBusMessage_append_data, SD_BUS_PY_METH, PyDoc_STR("Append basic data based on signature.")}, + {"open_container", (SD_BUS_PY_FUNC_TYPE)SdBusMessage_open_container, SD_BUS_PY_METH, PyDoc_STR("Open container for writing.")}, + {"close_container", (PyCFunction)SdBusMessage_close_container, METH_NOARGS, PyDoc_STR("Close container.")}, + {"enter_container", (SD_BUS_PY_FUNC_TYPE)SdBusMessage_enter_container, SD_BUS_PY_METH, PyDoc_STR("Enter container for reading.")}, + {"exit_container", (PyCFunction)SdBusMessage_exit_container, METH_NOARGS, PyDoc_STR("Exit container.")}, + {"dump", (PyCFunction)SdBusMessage_dump, METH_NOARGS, PyDoc_STR("Dump message to stdout.")}, + {"seal", (PyCFunction)SdBusMessage_seal, METH_NOARGS, PyDoc_STR("Seal message contents.")}, + {"get_contents", (PyCFunction)SdBusMessage_get_contents2, METH_NOARGS, PyDoc_STR("Iterate over message contents.")}, + {"parse_to_tuple", (PyCFunction)SdBusMessage_parse_to_tuple, METH_NOARGS, PyDoc_STR("Parse message data to a tuple.")}, + {"create_reply", (PyCFunction)SdBusMessage_create_reply, METH_NOARGS, PyDoc_STR("Create reply message.")}, + {"create_error_reply", (SD_BUS_PY_FUNC_TYPE)SdBusMessage_create_error_reply, SD_BUS_PY_METH, + PyDoc_STR("Create error reply with error name and error message.")}, + {"send", (PyCFunction)SdBusMessage_send, METH_NOARGS, PyDoc_STR("Queue message to be sent.")}, {NULL, NULL, 0, NULL}, }; @@ -1083,12 +1114,12 @@ static PyObject* SdBusMessage_sender_getter(SdBusMessageObject* self, void* Py_U } static PyGetSetDef SdBusMessage_properies[] = { - {"expect_reply", (getter)SdBusMessage_expect_reply_getter, (setter)SdBusMessage_expect_reply_setter, "Expect reply message?", NULL}, - {"destination", (getter)SdBusMessage_destination_getter, NULL, "Message destination service name", NULL}, - {"path", (getter)SdBusMessage_path_getter, NULL, "Message destination object path", NULL}, - {"interface", (getter)SdBusMessage_interface_getter, NULL, "Message destination interface name", NULL}, - {"member", (getter)SdBusMessage_member_getter, NULL, "Message destination member name", NULL}, - {"sender", (getter)SdBusMessage_sender_getter, NULL, "Message sender name", NULL}, + {"expect_reply", (getter)SdBusMessage_expect_reply_getter, (setter)SdBusMessage_expect_reply_setter, PyDoc_STR("Expect reply message?"), NULL}, + {"destination", (getter)SdBusMessage_destination_getter, NULL, PyDoc_STR("Message destination service name."), NULL}, + {"path", (getter)SdBusMessage_path_getter, NULL, PyDoc_STR("Message destination object path."), NULL}, + {"interface", (getter)SdBusMessage_interface_getter, NULL, PyDoc_STR("Message destination interface name."), NULL}, + {"member", (getter)SdBusMessage_member_getter, NULL, PyDoc_STR("Message destination member name."), NULL}, + {"sender", (getter)SdBusMessage_sender_getter, NULL, PyDoc_STR("Message sender name."), NULL}, {0}, }; diff --git a/src/sdbus/unittest.py b/src/sdbus/unittest.py index 4230f0e..62ac280 100644 --- a/src/sdbus/unittest.py +++ b/src/sdbus/unittest.py @@ -19,16 +19,36 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA from __future__ import annotations +from asyncio import Event, TimeoutError, wait_for +from contextlib import ExitStack, contextmanager +from operator import setitem from os import environ, kill from pathlib import Path from signal import SIGTERM from subprocess import DEVNULL from subprocess import run as subprocess_run from tempfile import TemporaryDirectory -from typing import ClassVar +from typing import TYPE_CHECKING from unittest import IsolatedAsyncioTestCase +from weakref import ref as weak_ref + +from .dbus_proxy_async_signal import DbusLocalSignalAsync, DbusProxySignalAsync +from .default_bus import _get_defaul_bus_tls, _set_default_bus_tls +from .sd_bus_internals import SdBusMessage, sd_bus_open_user + +if TYPE_CHECKING: + from collections.abc import Iterator + from contextlib import AbstractAsyncContextManager + from typing import Any, Optional, TypeVar, Union + + from .dbus_proxy_async_signal import ( + DbusBoundSignalAsyncBase, + DbusSignalAsync, + ) + from .sd_bus_internals import SdBus, SdBusSlot + + T = TypeVar('T') -from sdbus import sd_bus_open_user, set_default_bus dbus_config = ''' @@ -45,49 +65,178 @@ ''' -class IsolatedDbusTestCase(IsolatedAsyncioTestCase): - dbus_executable_name: ClassVar[str] = 'dbus-daemon' +class DbusSignalRecorderBase: + def __init__( + self, + timeout: Union[int, float], + ): + self._timeout = timeout + self._captured_data: list[Any] = [] + self._ready_event = Event() + self._callback_method = self._callback + + async def start(self) -> None: + raise NotImplementedError + + async def stop(self) -> None: + raise NotImplementedError + + async def __aenter__(self) -> DbusSignalRecorderBase: + raise NotImplementedError + + async def __aexit__( + self, + exc_type: Any, + exc_value: Any, + traceback: Any, + ) -> None: + if exc_type is not None: + return + + try: + await wait_for(self._ready_event.wait(), timeout=self._timeout) + except TimeoutError: + raise AssertionError("D-Bus signal not captured.") from None + + def _callback(self, data: Any) -> None: + if isinstance(data, SdBusMessage): + data = data.get_contents() + + self._captured_data.append(data) + self._ready_event.set() + + @property + def output(self) -> list[Any]: + return self._captured_data.copy() + + +class DbusSignalRecorderRemote(DbusSignalRecorderBase): + def __init__( + self, + timeout: Union[int, float], + bus: SdBus, + remote_signal: DbusProxySignalAsync[Any], + ): + super().__init__(timeout) + self._bus = bus + self._match_slot: Optional[SdBusSlot] = None + self._remote_signal = remote_signal + + async def __aenter__(self) -> DbusSignalRecorderBase: + self._match_slot = await self._remote_signal._register_match_slot( + self._bus, + self._callback_method, + ) - def setUp(self) -> None: - self.temp_dir = TemporaryDirectory() - self.temp_dir_path = Path(self.temp_dir.name) + return self + + async def __aexit__( + self, + exc_type: Any, + exc_value: Any, + traceback: Any, + ) -> None: + try: + await super().__aexit__(exc_type, exc_value, traceback) + finally: + if self._match_slot is not None: + self._match_slot.close() + + +class DbusSignalRecorderLocal(DbusSignalRecorderBase): + def __init__( + self, + timeout: Union[int, float], + local_signal: DbusLocalSignalAsync[Any], + ): + super().__init__(timeout) + self._local_signal_ref: weak_ref[DbusSignalAsync[Any]] = ( + weak_ref(local_signal.dbus_signal) + ) + + async def __aenter__(self) -> DbusSignalRecorderBase: + local_signal = self._local_signal_ref() - self.dbus_socket_path = self.temp_dir_path / 'test_dbus.socket' - self.pid_path = self.temp_dir_path / 'dbus.pid' + if local_signal is None: + raise RuntimeError - self.dbus_config_file = self.temp_dir_path / 'dbus.config' + local_signal.local_callbacks.add(self._callback_method) + return self - with open(self.dbus_config_file, mode='x') as conf_file: - conf_file.write(dbus_config.format( - socket_path=self.dbus_socket_path, - pidfile_path=self.pid_path)) + +@contextmanager +def _isolated_dbus( + dbus_executable_name: str = "dbus-daemon", +) -> Iterator[SdBus]: + with ExitStack() as exit_stack: + temp_dir_path = Path( + exit_stack.enter_context( + TemporaryDirectory(prefix="python-sdbus-") + ) + ) + + dbus_socket_path = temp_dir_path / "test_dbus.socket" + pid_path = temp_dir_path / "dbus.pid" + dbus_config_file = temp_dir_path / "dbus.config" + dbus_config_file.write_text( + dbus_config.format( + socket_path=dbus_socket_path, + pidfile_path=pid_path + ) + ) subprocess_run( args=( - self.dbus_executable_name, - '--config-file', self.dbus_config_file, + dbus_executable_name, + '--config-file', dbus_config_file, '--fork', ), stdin=DEVNULL, check=True, ) + # D-Bus daemon exits once it forks and is initialized. + + dbus_pid = int(pid_path.read_text()) + exit_stack.callback(kill, dbus_pid, SIGTERM) + + old_session_bus_address = environ.get("DBUS_SESSION_BUS_ADDRESS") + if old_session_bus_address is not None: + exit_stack.callback( + setitem, + environ, + "DBUS_SESSION_BUS_ADDRESS", + old_session_bus_address, + ) + else: + exit_stack.callback( + environ.pop, + "DBUS_SESSION_BUS_ADDRESS", + ) + environ["DBUS_SESSION_BUS_ADDRESS"] = f"unix:path={dbus_socket_path}" + + old_bus = _get_defaul_bus_tls() + bus = sd_bus_open_user() + _set_default_bus_tls(bus) + exit_stack.callback(_set_default_bus_tls, old_bus) + yield bus - self.old_session_bus_address = environ.get('DBUS_SESSION_BUS_ADDRESS') - environ[ - 'DBUS_SESSION_BUS_ADDRESS'] = f"unix:path={self.dbus_socket_path}" - - self.bus = sd_bus_open_user() - set_default_bus(self.bus) - async def asyncSetUp(self) -> None: - set_default_bus(self.bus) - - def tearDown(self) -> None: - with open(self.pid_path) as pid_file: - dbus_pid = int(pid_file.read()) - - kill(dbus_pid, SIGTERM) - self.temp_dir.cleanup() - environ.pop('DBUS_SESSION_BUS_ADDRESS') - if self.old_session_bus_address is not None: - environ['DBUS_SESSION_BUS_ADDRESS'] = self.old_session_bus_address +class IsolatedDbusTestCase(IsolatedAsyncioTestCase): + def setUp(self) -> None: + # TODO: Use enterContext from Python 3.11 + _isolated_dbus_cm = _isolated_dbus() + self.bus = _isolated_dbus_cm.__enter__() + self.addCleanup(_isolated_dbus_cm.__exit__, None, None, None) + + def assertDbusSignalEmits( + self, + signal: DbusBoundSignalAsyncBase[Any], + timeout: Union[int, float] = 1, + ) -> AbstractAsyncContextManager[DbusSignalRecorderBase]: + + if isinstance(signal, DbusLocalSignalAsync): + return DbusSignalRecorderLocal(timeout, signal) + elif isinstance(signal, DbusProxySignalAsync): + return DbusSignalRecorderRemote(timeout, self.bus, signal) + else: + raise TypeError("Unknown or unsupported signal class.") diff --git a/src/sdbus/utils/__init__.py b/src/sdbus/utils/__init__.py new file mode 100644 index 0000000..11fa044 --- /dev/null +++ b/src/sdbus/utils/__init__.py @@ -0,0 +1,34 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later + +# Copyright (C) 2024 igo95862 + +# This file is part of python-sdbus + +# 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 __future__ import annotations + +from .parse import ( + parse_get_managed_objects, + parse_interfaces_added, + parse_interfaces_removed, + parse_properties_changed, +) + +__all__ = ( + "parse_get_managed_objects", + "parse_interfaces_added", + "parse_interfaces_removed", + "parse_properties_changed", +) diff --git a/src/sdbus/utils/inspect.py b/src/sdbus/utils/inspect.py new file mode 100644 index 0000000..5306db7 --- /dev/null +++ b/src/sdbus/utils/inspect.py @@ -0,0 +1,141 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later + +# Copyright (C) 2024 igo95862 + +# This file is part of python-sdbus + +# 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 __future__ import annotations + +from typing import TYPE_CHECKING + +from ..dbus_common_elements import DbusLocalObjectMeta, DbusRemoteObjectMeta +from ..dbus_proxy_async_interface_base import DbusInterfaceBaseAsync +from ..dbus_proxy_sync_interface_base import DbusInterfaceBase +from ..default_bus import get_default_bus + +if TYPE_CHECKING: + from typing import Optional, Union + + from ..sd_bus_internals import SdBus + + +def _inspect_dbus_path_proxy( + obj: object, + dbus_meta: DbusRemoteObjectMeta, + bus: SdBus, +) -> str: + if bus != dbus_meta.attached_bus: + raise LookupError( + f"D-Bus proxy {obj!r} at {dbus_meta.object_path!r} path " + f"is not attached to bus {bus!r}" + ) + + return dbus_meta.object_path + + +def _inspect_dbus_path_local( + obj: object, + dbus_meta: DbusLocalObjectMeta, + bus: SdBus, +) -> str: + attached_bus = dbus_meta.attached_bus + object_path = dbus_meta.serving_object_path + if attached_bus is None or object_path is None: + raise LookupError( + f"Local D-Bus object {obj!r} is not exported to any D-Bus" + ) + + if bus != attached_bus: + raise LookupError( + f"Local D-Bus object {obj!r} at {dbus_meta.serving_object_path!r} " + f"path is not attached to bus {bus!r}" + ) + + return object_path + + +def inspect_dbus_path( + obj: Union[DbusInterfaceBase, DbusInterfaceBaseAsync], + bus: Optional[SdBus] = None, +) -> str: + """Return the D-Bus path of an object. + + If called on a D-Bus proxy returns path of the proxied object. + + If called on a local D-Bus object returns the exported D-Bus path. + If object is not exported raises ``LookupError``. + + If called on an object that is unrelated to D-Bus raises ``TypeError``. + + The object's path is inspected in the context of the given bus and if the + object is attached to a different bus the ``LookupError`` will be raised. + If the bus argument is not given or is ``None`` the default bus will be + checked against. + + :param obj: + Object to inspect. + :param bus: + Bus to inspect against. + If not given or is ``None`` the default bus will be used. + :returns: + D-Bus path of the object. + + *New in version 0.13.0.* + """ + if bus is None: + bus = get_default_bus() + + if isinstance(obj, DbusInterfaceBase): + return _inspect_dbus_path_proxy(obj, obj._dbus, bus) + elif isinstance(obj, DbusInterfaceBaseAsync): + dbus_meta = obj._dbus + if isinstance(dbus_meta, DbusRemoteObjectMeta): + return _inspect_dbus_path_proxy(obj, dbus_meta, bus) + else: + return _inspect_dbus_path_local(obj, dbus_meta, bus) + else: + raise TypeError(f"Expected D-Bus object got {obj!r}") + + +def inspect_dbus_bus( + obj: Union[DbusInterfaceBase, DbusInterfaceBaseAsync] +) -> Optional[SdBus]: + """Return D-Bus bus used by the object. + + If called on D-Bus proxies or exported local D-Bus objects returns + bus object. + + If called on local D-Bus objects that had not been exported returns None. + + If called on an object that is unrelated to D-Bus raises ``TypeError``. + + :param obj: + Object to inspect. + :returns: + D-Bus bus object. + + *New in version 0.14.1.* + """ + if isinstance(obj, (DbusInterfaceBase, DbusInterfaceBaseAsync)): + return obj._dbus.attached_bus + else: + raise TypeError(f"Expected D-Bus object got {obj!r}") + + +__all__ = ( + 'inspect_dbus_bus', + "inspect_dbus_path", +) diff --git a/src/sdbus/utils/parse.py b/src/sdbus/utils/parse.py new file mode 100644 index 0000000..f56e5fe --- /dev/null +++ b/src/sdbus/utils/parse.py @@ -0,0 +1,409 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later + +# Copyright (C) 2023 igo95862 + +# This file is part of python-sdbus + +# 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 __future__ import annotations + +from typing import TYPE_CHECKING + +from ..dbus_common_funcs import _parse_properties_vardict +from ..dbus_proxy_async_interface_base import ( + DBUS_CLASS_TO_META, + DBUS_INTERFACE_NAME_TO_CLASS, + DbusInterfaceBaseAsync, +) +from ..dbus_proxy_sync_interface_base import DbusInterfaceBase + +if TYPE_CHECKING: + from collections.abc import Iterable + from typing import Any, Literal, Optional, Union + + from ..dbus_proxy_async_interfaces import DBUS_PROPERTIES_CHANGED_TYPING + + InterfacesBaseClasses = Union[DbusInterfaceBaseAsync, DbusInterfaceBase] + InterfacesBaseTypes = type[InterfacesBaseClasses] + InterfacesInputElements = Union[InterfacesBaseClasses, InterfacesBaseTypes] + InterfacesInput = Union[ + InterfacesInputElements, + Iterable[InterfacesInputElements], + ] + InterfacesToClassMap = dict[ + frozenset[str], + InterfacesBaseTypes, + ] + OnUnknownMember = Literal['error', 'ignore', 'reuse'] + OnUnknownInterface = Literal['error', 'none'] + ParseGetManaged = dict[ + str, + tuple[ + Optional[InterfacesBaseTypes], + dict[str, Any], + ], + ] + + +def _interfaces_input_to_types( + interfaces: InterfacesInput, +) -> tuple[InterfacesBaseTypes, ...]: + if isinstance( + interfaces, + (DbusInterfaceBaseAsync, DbusInterfaceBase, type) + ): + return ( + interfaces if isinstance(interfaces, type) else type(interfaces), + ) + else: + return tuple(i if isinstance(i, type) else type(i) for i in interfaces) + + +def parse_properties_changed( + interface: InterfacesInputElements, + properties_changed_data: DBUS_PROPERTIES_CHANGED_TYPING, + on_unknown_member: OnUnknownMember = 'error', +) -> dict[str, Any]: + """Parse data from :py:meth:`properties_changed \ + ` signal. + + Parses changed properties from a single D-Bus object. The object's + interface class must be known in advance and passed as a first + argument. + + Member names will be translated to python defined names. + Invalidated properties will have a value of None. + + :param interface: + Takes either D-Bus interface class or its object. + :param properties_changed_data: + Tuple caught from signal. + :param on_unknown_member: + If an unknown D-Bus property was encountered either raise + an ``"error"`` (default), ``"ignore"`` the property + or ``"reuse"`` the D-Bus name for the member. + :returns: + Dictionary of changed properties with keys translated to python + names. Invalidated properties will have value of None. + """ + interface_name, changed_properties, invalidated_properties = ( + properties_changed_data + ) + + meta = DBUS_CLASS_TO_META[DBUS_INTERFACE_NAME_TO_CLASS[interface_name]] + + for invalidated_property in invalidated_properties: + changed_properties[invalidated_property] = ('0', None) + + return _parse_properties_vardict( + meta.dbus_member_to_python_attr, + properties_changed_data[1], + on_unknown_member, + ) + + +SKIP_INTERFACES = frozenset(( + 'org.freedesktop.DBus.Properties', + 'org.freedesktop.DBus.Introspectable', + 'org.freedesktop.DBus.Peer', + 'org.freedesktop.DBus.ObjectManager', +)) + + +def _create_interfaces_map( + interfaces: tuple[InterfacesBaseTypes, ...], +) -> InterfacesToClassMap: + + interfaces_to_class_map: InterfacesToClassMap = {} + + for interface in interfaces: + interface_names_set = frozenset( + interface_name for interface_name, _ in + interface._dbus_iter_interfaces_meta() + if interface_name not in SKIP_INTERFACES + ) + interfaces_to_class_map[interface_names_set] = ( + interface if isinstance(interface, type) + else type(interface) + ) + + return interfaces_to_class_map + + +def _get_class_from_interfaces( + interfaces_to_class_map: InterfacesToClassMap, + interface_names_iter: Iterable[str], + raise_key_error: bool, + use_subset: bool, +) -> Optional[InterfacesBaseTypes]: + class_set = frozenset(interface_names_iter) - SKIP_INTERFACES + if use_subset: + for interface_available in sorted( + interfaces_to_class_map.keys(), + key=len, + reverse=True, + ): + if interface_available.issubset(class_set): + class_set = interface_available + break + + try: + return interfaces_to_class_map[class_set] + except KeyError: + if raise_key_error: + raise + + return None + + +def _get_member_map_from_class( + python_class: Optional[InterfacesBaseTypes], +) -> dict[str, dict[str, str]]: + if python_class is None: + return {} + else: + return { + interface_name: meta.dbus_member_to_python_attr + for interface_name, meta in + python_class._dbus_iter_interfaces_meta() + } + + +def _translate_and_merge_members( + properties_data: dict[str, dict[str, Any]], + dbus_to_python_map: dict[str, dict[str, str]], + on_unknown_member: OnUnknownMember, +) -> dict[str, Any]: + python_properties: dict[str, Any] = {} + for interface_name, properties in properties_data.items(): + interface_member_map = dbus_to_python_map.get( + interface_name, {}, + ) + python_properties.update( + _parse_properties_vardict( + interface_member_map, + properties, + on_unknown_member, + ) + ) + + return python_properties + + +def parse_interfaces_added( + interfaces: InterfacesInput, + interfaces_added_data: tuple[str, dict[str, dict[str, Any]]], + on_unknown_interface: OnUnknownInterface = 'error', + on_unknown_member: OnUnknownMember = 'error', + *, + use_interface_subsets: bool = False, +) -> tuple[str, Optional[InterfacesBaseTypes], dict[str, Any]]: + """Parse data from :py:meth:`interfaces_added \ + ` signal. + + Takes the possible interface classes and the signal data. + Returns the path of new object, the class of the + added object (if it matched one of passed interface classes) + and the dictionary of python named properties and their values. + + The passed interfaces can be async or blocking, the class + or an instantiated object, a single item or an iterable of interfaces. + + :param interfaces: + Possible interfaces that were added. + :param interfaces_added_data: + Tuple caught from signal. + :param on_unknown_interface: + If an unknown D-Bus interface was encountered either raise + an ``"error"`` (default) or return ``"none"`` instead of + interface class. + :param on_unknown_member: + If an unknown D-Bus property was encountered either raise + an ``"error"`` (default), ``"ignore"`` the property + or ``"reuse"`` the D-Bus name for the member. + :param use_interface_subsets: + Use the subset of interfaces as a valid match. For example, + the class that implements ``org.example.foo`` would be matched + with an data consising of both ``org.example.foo`` and + ``org.example.bar``. The classes implementing more interfaces + will have higher priority over the ones implementing fewer. + :returns: + Path of new added object, object's class (or ``None``) and dictionary + of python translated members and their values. + """ + interfaces_types = _interfaces_input_to_types(interfaces) + interfaces_to_class_map = _create_interfaces_map(interfaces_types) + + path, properties_data = interfaces_added_data + + python_class = ( + _get_class_from_interfaces( + interfaces_to_class_map, + properties_data.keys(), + on_unknown_interface == "error", + use_interface_subsets, + ) + ) + dbus_to_python_member_map = _get_member_map_from_class(python_class) + python_properties: dict[str, Any] = {} + for interface_name, properties in properties_data.items(): + interface_member_map = dbus_to_python_member_map.get( + interface_name, {}, + ) + python_properties.update( + _parse_properties_vardict( + interface_member_map, + properties, + on_unknown_member, + ) + ) + + return ( + path, + python_class, + _translate_and_merge_members( + properties_data, + dbus_to_python_member_map, + on_unknown_member, + ), + ) + + +def parse_interfaces_removed( + interfaces: InterfacesInput, + interfaces_removed_data: tuple[str, list[str]], + on_unknown_interface: OnUnknownInterface = 'error', + *, + use_interface_subsets: bool = False, +) -> tuple[str, Optional[InterfacesBaseTypes]]: + """Parse data from :py:meth:`interfaces_added \ + ` signal. + + Takes the possible interface classes and the signal data. + Returns the path and the matched class of removed object. + (if it matched one of passed interface classes) + + The passed interfaces can be async or blocking, the class + or an instantiated object, a single item or an iterable of interfaces. + + :param interfaces: + Possible interfaces that were removed. + :param interfaces_added_data: + Tuple caught from signal. + :param on_unknown_member: + If an unknown D-Bus interface was encountered either raise an + ``"error"`` (default) or return ``"none"`` instead of interface class. + :param use_interface_subsets: + Use the subset of interfaces as a valid match. For example, + the class that implements ``org.example.foo`` would be matched + with an data consising of both ``org.example.foo`` and + ``org.example.bar``. The classes implementing more interfaces + will have higher priority over the ones implementing fewer. + :returns: + Path of removed object and object's class (or ``None``). + """ + interfaces_types = _interfaces_input_to_types(interfaces) + interfaces_to_class_map = _create_interfaces_map(interfaces_types) + + path, interfaces_removed = interfaces_removed_data + + python_class = ( + _get_class_from_interfaces( + interfaces_to_class_map, + interfaces_removed, + on_unknown_interface == "error", + use_interface_subsets, + ) + ) + + return path, python_class + + +def parse_get_managed_objects( + interfaces: InterfacesInput, + managed_objects_data: dict[str, dict[str, dict[str, Any]]], + on_unknown_interface: OnUnknownInterface = 'error', + on_unknown_member: OnUnknownMember = 'error', + *, + use_interface_subsets: bool = False, +) -> ParseGetManaged: + """Parse data from :py:meth:`get_managed_objects \ + ` call. + + Takes the possible interface classes and the method's returned data. + Returns a dictionary where keys a paths of the managed objects and + value is a tuple of class of the object and dictionary of its python + named properties and their values. + + The passed interfaces can be async or blocking, the class + or an instantiated object, a single item or an iterable of interfaces. + + :param interfaces: + Possible interfaces of the managed objects. + :param managed_objects_data: + Data returned by ``get_managed_objects`` call. + :param on_unknown_interface: + If an unknown D-Bus interface was encountered either raise an + ``"error"`` (default) or return ``"none"`` instead of interface class. + :param on_unknown_member: + If an unknown D-Bus property was encountered either raise + an ``"error"`` (default), ``"ignore"`` the property + or ``"reuse"`` the D-Bus name for the member. + :param use_interface_subsets: + Use the subset of interfaces as a valid match. For example, + the class that implements ``org.example.foo`` would be matched + with an data consising of both ``org.example.foo`` and + ``org.example.bar``. The classes implementing more interfaces + will have higher priority over the ones implementing fewer. + :returns: + Dictionary where keys are paths and values are tuples of managed + objects classes and their properties data. + + *New in version 0.12.0.* + """ + interfaces_types = _interfaces_input_to_types(interfaces) + interfaces_to_class_map = _create_interfaces_map(interfaces_types) + + managed_objects_map: ParseGetManaged = {} + + for path, properties_data in managed_objects_data.items(): + python_class = ( + _get_class_from_interfaces( + interfaces_to_class_map, + properties_data.keys(), + on_unknown_interface == "error", + use_interface_subsets, + ) + ) + dbus_to_python_member_map = _get_member_map_from_class(python_class) + + managed_objects_map[path] = ( + python_class, + _translate_and_merge_members( + properties_data, + dbus_to_python_member_map, + on_unknown_member, + ), + ) + + return managed_objects_map + + +__all__ = ( + 'parse_properties_changed', + 'parse_interfaces_added', + 'parse_interfaces_removed', + 'parse_get_managed_objects', +) diff --git a/src/sdbus_async/dbus_daemon/__init__.py b/src/sdbus_async/dbus_daemon/__init__.py index 0816d03..e813417 100644 --- a/src/sdbus_async/dbus_daemon/__init__.py +++ b/src/sdbus_async/dbus_daemon/__init__.py @@ -19,7 +19,7 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA from __future__ import annotations -from typing import List, Optional, Tuple +from typing import Optional from sdbus import ( DbusInterfaceCommonAsync, @@ -35,15 +35,15 @@ class FreedesktopDbus(DbusInterfaceCommonAsync, """D-Bus daemon.""" def __init__(self, bus: Optional[SdBus] = None): - """This is the dbus daemon interface. Used for querying dbus state. + """This is the D-Bus daemon interface. Used for querying D-Bus state. - Dbus interface object path and service name is + D-Bus interface object path and service name is predetermined. (at ``'org.freedesktop.DBus'``, ``'/org/freedesktop/DBus'``) :param SdBus bus: - Optional dbus connection. - If not passed the default dbus will be used. + Optional D-Bus connection. + If not passed the default D-Bus will be used. """ super().__init__() self._proxify( @@ -91,7 +91,7 @@ async def get_name_owner(self, service_name: str) -> str: raise NotImplementedError @dbus_method_async() - async def list_activatable_names(self) -> List[str]: + async def list_activatable_names(self) -> list[str]: """Lists all activatable services names. :return: List of all names. @@ -99,7 +99,7 @@ async def list_activatable_names(self) -> List[str]: raise NotImplementedError @dbus_method_async() - async def list_names(self) -> List[str]: + async def list_names(self) -> list[str]: """List all services and connections currently of the bus. :return: List of all current names. @@ -134,8 +134,8 @@ async def start_service_by_name( raise NotImplementedError @dbus_property_async('as') - def features(self) -> List[str]: - """List of dbus daemon features. + def features(self) -> list[str]: + """List of D-Bus daemon features. Features include: @@ -144,13 +144,13 @@ def features(self) -> List[str]: header fields. * 'SELinux' - Messages filtered by SELinux on this bus. * 'SystemdActivation' - services activated by systemd if their \ - .service file specifies a dbus name. + .service file specifies a D-Bus name. """ raise NotImplementedError @dbus_property_async('as') - def interfaces(self) -> List[str]: - """Extra dbus daemon interfaces""" + def interfaces(self) -> list[str]: + """Extra D-Bus daemon interfaces""" raise NotImplementedError @dbus_signal_async('s') @@ -164,7 +164,7 @@ def name_lost(self) -> str: raise NotImplementedError @dbus_signal_async('sss') - def name_owner_changed(self) -> Tuple[str, str, str]: + def name_owner_changed(self) -> tuple[str, str, str]: """Signal when some name on a bus changes owner. Is a tuple of: diff --git a/src/sdbus_block/dbus_daemon/__init__.py b/src/sdbus_block/dbus_daemon/__init__.py index 39ca578..e8bdeba 100644 --- a/src/sdbus_block/dbus_daemon/__init__.py +++ b/src/sdbus_block/dbus_daemon/__init__.py @@ -19,7 +19,7 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA from __future__ import annotations -from typing import List, Optional +from typing import Optional from sdbus import DbusInterfaceCommon, SdBus, dbus_method, dbus_property @@ -29,15 +29,15 @@ class FreedesktopDbus(DbusInterfaceCommon, """D-Bus daemon.""" def __init__(self, bus: Optional[SdBus] = None): - """This is the dbus daemon interface. Used for querying dbus state. + """This is the D-Bus daemon interface. Used for querying D-Bus state. - Dbus interface object path and service name is + D-Bus interface object path and service name is predetermined. (at ``'org.freedesktop.DBus'``, ``'/org/freedesktop/DBus'``) :param SdBus bus: - Optional dbus connection. - If not passed the default dbus will be used. + Optional D-Bus connection. + If not passed the default D-Bus will be used. """ super().__init__( 'org.freedesktop.DBus', @@ -84,7 +84,7 @@ def get_name_owner(self, service_name: str) -> str: raise NotImplementedError @dbus_method() - def list_activatable_names(self) -> List[str]: + def list_activatable_names(self) -> list[str]: """Lists all activatable services names. :return: List of all names. @@ -92,7 +92,7 @@ def list_activatable_names(self) -> List[str]: raise NotImplementedError @dbus_method() - def list_names(self) -> List[str]: + def list_names(self) -> list[str]: """List all services and connections currently of the bus. :return: List of all current names. @@ -125,8 +125,8 @@ def start_service_by_name( raise NotImplementedError @dbus_property('as') - def features(self) -> List[str]: - """List of dbus daemon features. + def features(self) -> list[str]: + """List of D-Bus daemon features. Features include: @@ -135,11 +135,11 @@ def features(self) -> List[str]: header fields. * 'SELinux' - Messages filtered by SELinux on this bus. * 'SystemdActivation' - services activated by systemd if their \ - .service file specifies a dbus name. + .service file specifies a D-Bus name. """ raise NotImplementedError @dbus_property('as') - def interfaces(self) -> List[str]: - """Extra dbus daemon interfaces""" + def interfaces(self) -> list[str]: + """Extra D-Bus daemon interfaces""" raise NotImplementedError diff --git a/test/benchmarks/__init__.py b/test/benchmarks/__init__.py new file mode 100644 index 0000000..d1a5ccb --- /dev/null +++ b/test/benchmarks/__init__.py @@ -0,0 +1,20 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later + +# Copyright (C) 2024 igo95862 + +# This file is part of python-sdbus + +# 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 __future__ import annotations diff --git a/test/benchmarks/bench_async_ping.py b/test/benchmarks/bench_async_ping.py new file mode 100644 index 0000000..06813e3 --- /dev/null +++ b/test/benchmarks/bench_async_ping.py @@ -0,0 +1,80 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later + +# Copyright (C) 2024 igo95862 + +# This file is part of python-sdbus + +# 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 __future__ import annotations + +from asyncio import gather +from asyncio import run as asyncio_run +from time import perf_counter + +import pyperf # type: ignore +from sdbus.unittest import _isolated_dbus + +from sdbus import DbusInterfaceCommonAsync + + +def bench_async_ping_gather(loops: int) -> float: + with _isolated_dbus() as bus: + dbus_interface = DbusInterfaceCommonAsync.new_proxy( + "org.freedesktop.DBus", + "/org/freedesktop/DBus", + bus, + ) + + async def run_ping_gather() -> float: + + gather_ping = gather( + *(dbus_interface.dbus_ping() for _ in range(loops)) + ) + start = perf_counter() + + await gather_ping + + return perf_counter() - start + + return asyncio_run(run_ping_gather()) + + +def bench_async_ping(loops: int) -> float: + with _isolated_dbus() as bus: + dbus_interface = DbusInterfaceCommonAsync.new_proxy( + "org.freedesktop.DBus", + "/org/freedesktop/DBus", + bus, + ) + + async def run_ping() -> float: + start = perf_counter() + + for _ in range(loops): + await dbus_interface.dbus_ping() + + return perf_counter() - start + + return asyncio_run(run_ping()) + + +def main() -> None: + runner = pyperf.Runner() + runner.bench_time_func('sdbus_async_ping', bench_async_ping) + runner.bench_time_func('sdbus_async_ping_gather', bench_async_ping_gather) + + +if __name__ == "__main__": + main() diff --git a/test/benchmarks/bench_block_ping.py b/test/benchmarks/bench_block_ping.py new file mode 100644 index 0000000..36c68d6 --- /dev/null +++ b/test/benchmarks/bench_block_ping.py @@ -0,0 +1,52 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later + +# Copyright (C) 2025 igo95862 + +# This file is part of python-sdbus + +# 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 __future__ import annotations + +from time import perf_counter + +import pyperf # type: ignore +from sdbus.unittest import _isolated_dbus + +from sdbus import DbusInterfaceCommon + + +def bench_block_ping(loops: int) -> float: + with _isolated_dbus() as bus: + dbus_interface = DbusInterfaceCommon( + "org.freedesktop.DBus", + "/org/freedesktop/DBus", + bus, + ) + + start = perf_counter() + + for _ in range(loops): + dbus_interface.dbus_ping() + + return perf_counter() - start + + +def main() -> None: + runner = pyperf.Runner() + runner.bench_time_func('sdbus_block_ping', bench_block_ping) + + +if __name__ == "__main__": + main() diff --git a/test/benchmarks/bench_dbus_object_export.py b/test/benchmarks/bench_dbus_object_export.py new file mode 100644 index 0000000..24e0cad --- /dev/null +++ b/test/benchmarks/bench_dbus_object_export.py @@ -0,0 +1,78 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later + +# Copyright (C) 2025 igo95862 + +# This file is part of python-sdbus + +# 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 __future__ import annotations + +from time import perf_counter + +import pyperf # type: ignore +from sdbus.unittest import _isolated_dbus + +from sdbus import DbusInterfaceCommonAsync, dbus_method_async + + +class ExampleInterface( + DbusInterfaceCommonAsync, + interface_name="org.example.interface" +): + @dbus_method_async( + input_signature="s", + result_signature="s", + ) + async def upper(self, string: str) -> str: + return string.upper() + + +def bench_dbus_object_export_stop(loops: int) -> float: + with _isolated_dbus() as bus: + example_object = ExampleInterface() + + start = perf_counter() + + for _ in range(loops): + example_object = ExampleInterface() + handle = example_object.export_to_dbus("/", bus=bus) + handle.stop() + + return perf_counter() - start + + +def bench_dbus_object_export_gc(loops: int) -> float: + with _isolated_dbus() as bus: + example_object = ExampleInterface() + + start = perf_counter() + + for _ in range(loops): + example_object = ExampleInterface() + example_object.export_to_dbus("/", bus=bus) + + return perf_counter() - start + + +def main() -> None: + runner = pyperf.Runner() + runner.bench_time_func("sdbus_dbus_object_export_stop", + bench_dbus_object_export_stop) + runner.bench_time_func("sdbus_dbus_object_export_gc", + bench_dbus_object_export_gc) + + +if __name__ == "__main__": + main() diff --git a/test/common_test_util.py b/test/common_test_util.py index b4316a6..9df7976 100644 --- a/test/common_test_util.py +++ b/test/common_test_util.py @@ -19,7 +19,7 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA from __future__ import annotations -from unittest import main +from unittest import SkipTest, main def mem_test() -> None: @@ -35,3 +35,23 @@ def mem_test_single(test_class: type, test_name: str) -> None: t = test_class() t.setUp() getattr(t, test_name)() + + +def skip_if_no_asserts() -> None: + try: + assert False + except AssertionError: + return + + raise SkipTest("Assertions are not enabled") + + +def skip_if_no_name_validations() -> None: + skip_if_no_asserts() + + from sdbus.sd_bus_internals import is_interface_name_valid + + try: + is_interface_name_valid("org.test") + except NotImplementedError: + raise SkipTest("Validation functions not available") diff --git a/test/containers/Containerfile-alpine b/test/containers/Containerfile-alpine index 73e3f34..984b152 100644 --- a/test/containers/Containerfile-alpine +++ b/test/containers/Containerfile-alpine @@ -13,7 +13,8 @@ RUN apk update && \ musl-dev \ gcc \ pkgconfig \ - dbus + dbus \ + py3-jinja2 WORKDIR /root/python-sdbus/ diff --git a/test/leak_tests.py b/test/leak_tests.py index c9a7037..59dcf0c 100644 --- a/test/leak_tests.py +++ b/test/leak_tests.py @@ -29,19 +29,20 @@ ) from os import environ from resource import RUSAGE_SELF, getrusage -from typing import Any, List, cast +from typing import Any, cast from unittest import SkipTest +from sdbus.exceptions import DbusFailedError from sdbus.unittest import IsolatedDbusTestCase -from sdbus import DbusFailedError, request_default_bus_name_async +from sdbus import request_default_bus_name_async from .test_low_level_errors import ( DbusDerivePropertydError, InterfaceWithErrors, ) from .test_read_write_dbus_types import TestDbusTypes -from .test_sd_bus_async import TestPing, TestProxy, initialize_object +from .test_sdbus_async import TestPing, TestProxy, initialize_object ENABLE_LEAK_TEST_VAR = 'PYTHON_SDBUS_TEST_LEAKS' @@ -115,7 +116,6 @@ async def test_objects(self) -> None: await TestProxy.test_method_kwargs(pseudo_test) await TestProxy.test_method(pseudo_test) await TestProxy.test_subclass(pseudo_test) - await TestProxy.test_bad_subclass(pseudo_test) await TestProxy.test_properties(pseudo_test) await TestProxy.test_signal(pseudo_test) await TestProxy.test_exceptions(pseudo_test) @@ -189,7 +189,7 @@ async def the_test() -> None: nonlocal i i += 1 - tasks: List[Task[None]] = [] + tasks: list[Task[None]] = [] loop = get_running_loop() for _ in range(num_of_tasks): tasks.append(loop.create_task(the_test())) diff --git a/test/test_default_bus.py b/test/test_default_bus.py new file mode 100644 index 0000000..1e08d34 --- /dev/null +++ b/test/test_default_bus.py @@ -0,0 +1,60 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later + +# Copyright (C) 2025 igo95862 + +# This file is part of python-sdbus + +# 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 __future__ import annotations + +from contextvars import copy_context +from unittest import main + +from sdbus.unittest import IsolatedDbusTestCase + +from sdbus import get_default_bus, sd_bus_open_user, set_context_default_bus + + +def return_bus_id() -> int: + return id(get_default_bus()) + + +def set_context_and_return_id() -> int: + set_context_default_bus(sd_bus_open_user()) + return id(get_default_bus()) + + +class TestDefaultBus(IsolatedDbusTestCase): + def test_context_bus(self) -> None: + bus_id = id(get_default_bus()) + + self.assertEqual( + bus_id, + copy_context().run(return_bus_id), + ) + + self.assertNotEqual( + bus_id, + copy_context().run(set_context_and_return_id), + ) + + self.assertEqual( + bus_id, + id(get_default_bus()), + ) + + +if __name__ == "__main__": + main() diff --git a/test/test_deprecations.py b/test/test_deprecations.py new file mode 100644 index 0000000..aec99ee --- /dev/null +++ b/test/test_deprecations.py @@ -0,0 +1,25 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later + +# Copyright (C) 2023 igo95862 + +# This file is part of python-sdbus + +# 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 __future__ import annotations + +from unittest import main + +if __name__ == '__main__': + main() diff --git a/test/test_high_level_errors.py b/test/test_high_level_errors.py index 02df8b0..3bfb873 100644 --- a/test/test_high_level_errors.py +++ b/test/test_high_level_errors.py @@ -22,10 +22,10 @@ from asyncio import get_running_loop, wait_for from typing import Any +from sdbus.exceptions import DbusFailedError from sdbus.unittest import IsolatedDbusTestCase from sdbus import ( - DbusFailedError, DbusInterfaceCommonAsync, dbus_method_async, request_default_bus_name_async, diff --git a/test/test_interface_generator.py b/test/test_interface_generator.py index f208477..5d072c0 100644 --- a/test/test_interface_generator.py +++ b/test/test_interface_generator.py @@ -21,14 +21,17 @@ from importlib.util import find_spec from unittest import SkipTest, TestCase, main +from unittest.mock import MagicMock, patch +from sdbus.__main__ import generator_main from sdbus.interface_generator import ( DbusSigToTyping, camel_case_to_snake_case, - generate_async_py_file, + generate_py_file, interface_name_to_class, interfaces_from_str, ) +from sdbus.unittest import IsolatedDbusTestCase test_xml = """ + + + + + + + + + @@ -62,10 +77,23 @@ class TestConverter(TestCase): def test_camel_to_snake(self) -> None: - self.assertEqual( - 'activate_connection', - camel_case_to_snake_case('ActivateConnection'), - ) + with self.subTest("CamelCase"): + self.assertEqual( + 'activate_connection', + camel_case_to_snake_case('ActivateConnection'), + ) + + with self.subTest("Already snake case"): + self.assertEqual( + 'activate_connection', + camel_case_to_snake_case('activate_connection'), + ) + + with self.subTest("Upper snake case"): + self.assertEqual( + 'activate_connection', + camel_case_to_snake_case('ACTIVATE_CONNECTION'), + ) def test_interface_name_to_class(self) -> None: self.assertEqual( @@ -84,7 +112,7 @@ def test_signature_to_typing(self) -> None: with self.subTest('Parse variant'): self.assertEqual( - 'Tuple[str, Any]', DbusSigToTyping.typing_complete('v') + 'tuple[str, Any]', DbusSigToTyping.typing_complete('v') ) with self.subTest('Splitter test'): @@ -96,40 +124,40 @@ def test_signature_to_typing(self) -> None: with self.subTest('Parse struct'): self.assertEqual( DbusSigToTyping.typing_complete('(sx)'), - 'Tuple[str, int]', + 'tuple[str, int]', ) with self.subTest('Parse list'): self.assertEqual( DbusSigToTyping.typing_complete('a(sx)'), - 'List[Tuple[str, int]]', + 'list[tuple[str, int]]', ) with self.subTest('Parse dict'): self.assertEqual( DbusSigToTyping.typing_complete('a{s(xh)}'), - 'Dict[str, Tuple[int, int]]', + 'dict[str, tuple[int, int]]', ) with self.subTest('Parse signature'): self.assertEqual( DbusSigToTyping.sig_to_typing('a{s(xh)}'), - 'Dict[str, Tuple[int, int]]', + 'dict[str, tuple[int, int]]', ) self.assertEqual( DbusSigToTyping.sig_to_typing('a{s(xh)}xs'), - 'Tuple[Dict[str, Tuple[int, int]], int, str]', + 'tuple[dict[str, tuple[int, int]], int, str]', ) self.assertEqual( DbusSigToTyping.sig_to_typing('a{s(xh)}xs'), - 'Tuple[Dict[str, Tuple[int, int]], int, str]', + 'tuple[dict[str, tuple[int, int]], int, str]', ) self.assertEqual( DbusSigToTyping.sig_to_typing('as'), - 'List[str]', + 'list[str]', ) self.assertEqual( @@ -141,7 +169,174 @@ def test_parsing(self) -> None: if find_spec('jinja2') is None: raise SkipTest('Jinja2 not installed') - generate_async_py_file(interfaces_from_str(test_xml)) + interfaces_intro = interfaces_from_str(test_xml) + + with self.subTest('Test introspection details'): + test_interface = interfaces_intro[0] + + for test_property in test_interface.properties: + if test_property.method_name == 'BoundBy': + self.assertEqual( + test_property.emits_changed, + 'const', + ) + elif test_property.method_name == 'Bar': + self.assertEqual( + test_property.emits_changed, + True, + ) + elif test_property.method_name == 'FooInvalidates': + self.assertEqual( + test_property.emits_changed, + 'invalidates', + ) + elif test_property.method_name == 'FooFoo': + self.assertEqual( + test_property.emits_changed, + False, + ) + + generated = generate_py_file(interfaces_intro) + self.assertIn('flags=DbusPropertyEmitsInvalidationFlag', generated) + self.assertIn('flags=DbusPropertyConstFlag', generated) + + +class TestGeneratorAgainstDbus(IsolatedDbusTestCase): + def setUp(self) -> None: + if find_spec('jinja2') is None: + raise SkipTest('Jinja2 not installed') + + super().setUp() + + def test_generate_from_connection(self) -> None: + with patch("sdbus.__main__.stdout") as stdout_mock: + generator_main( + [ + "gen-from-connection", + "org.freedesktop.DBus", + "/org/freedesktop/DBus", + ] + ) + + write_mock: MagicMock = stdout_mock.write + write_mock.assert_called_once() + + generated_interface = write_mock.call_args.args[0] + + self.assertIn( + "OrgFreedesktopDBusDebugStatsInterface", + generated_interface, + ) + self.assertIn( + "get_connection_unix_process_id", + generated_interface, + ) + self.assertIn( + "async", + generated_interface, + ) + + def test_generate_from_connection_blocking(self) -> None: + with patch("sdbus.__main__.stdout") as stdout_mock: + generator_main( + [ + "gen-from-connection", + "--block", + "org.freedesktop.DBus", + "/org/freedesktop/DBus", + ] + ) + + write_mock: MagicMock = stdout_mock.write + write_mock.assert_called_once() + + generated_interface = write_mock.call_args.args[0] + + self.assertNotIn( + "async", + generated_interface, + ) + self.assertIn( + "dbus_property", + generated_interface, + ) + + +INTERFACE_NO_MEMBERS_XML = """ + + + + + + + +""" + + +class TestGeneratorSyntaxCompile(TestCase): + def setUp(self) -> None: + if find_spec('jinja2') is None: + raise SkipTest('Jinja2 not installed') + + super().setUp() + + def test_syntax_compile_async(self) -> None: + source_code = generate_py_file( + interfaces_from_str(test_xml), + do_async=True, + ) + compile(source_code, filename="", mode="exec") + + def test_syntax_compile_block(self) -> None: + source_code = generate_py_file( + interfaces_from_str(test_xml), + do_async=False, + ) + compile(source_code, filename="", mode="exec") + + def test_syntax_no_members_interface(self) -> None: + + regular_interface = interfaces_from_str(test_xml) + no_members_interface = interfaces_from_str(INTERFACE_NO_MEMBERS_XML) + + self.assertFalse(no_members_interface[0].methods) + self.assertFalse(no_members_interface[0].properties) + self.assertFalse(no_members_interface[0].signals) + + compile( + generate_py_file( + regular_interface + no_members_interface, + do_async=True, + ), + filename="", + mode="exec", + ) + compile( + generate_py_file( + no_members_interface + regular_interface, + do_async=True, + ), + filename="", + mode="exec", + ) + + compile( + generate_py_file( + regular_interface + no_members_interface, + do_async=False, + ), + filename="", + mode="exec", + ) + compile( + generate_py_file( + no_members_interface + regular_interface, + do_async=False, + ), + filename="", + mode="exec", + ) if __name__ == "__main__": diff --git a/test/test_low_level_api.py b/test/test_low_level_api.py index 62b7f33..1030bb3 100644 --- a/test/test_low_level_api.py +++ b/test/test_low_level_api.py @@ -19,7 +19,8 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA from __future__ import annotations -from unittest import SkipTest, main +from asyncio import get_running_loop +from unittest import SkipTest, TestCase, main from sdbus.sd_bus_internals import ( SdBus, @@ -31,13 +32,25 @@ from sdbus.unittest import IsolatedDbusTestCase -class TestDbusTypes(IsolatedDbusTestCase): +class TestAsyncLowLevel(IsolatedDbusTestCase): def test_init_bus(self) -> None: not_connected_bus = SdBus() self.assertIsNone(not_connected_bus.address) self.assertIsNotNone(self.bus.address) + async def test_bus_fd_unregister_close(self) -> None: + await self.bus.request_name_async("org.example", 0) + bus_fd = self.bus.get_fd() + + self.bus.close() + + loop = get_running_loop() + self.assertFalse(loop.remove_reader(bus_fd)) + self.assertFalse(loop.remove_writer(bus_fd)) + + +class TestLowLeveApi(TestCase): def test_validation_funcs(self) -> None: try: self.assertTrue( @@ -73,12 +86,25 @@ def test_validation_funcs(self) -> None: ) except NotImplementedError: raise SkipTest( - ( - "Validation funcs not implemented. " - "Probably too old libsystemd. (< 246)" - ) + "Validation funcs not implemented. " + "Probably too old libsystemd. (< 246)" ) + def test_bus_method_call_timeout(self) -> None: + bus = SdBus() + + self.assertIsNotNone(bus.method_call_timeout_usec) + + test_timeout_usec = 10 * 10**6 # 10 seconds + bus.method_call_timeout_usec = test_timeout_usec + self.assertEqual(test_timeout_usec, bus.method_call_timeout_usec) + + with self.assertRaises(TypeError): + bus.method_call_timeout_usec = "test" # type: ignore + + with self.assertRaises(ValueError): + del bus.method_call_timeout_usec + if __name__ == "__main__": main() diff --git a/test/test_low_level_errors.py b/test/test_low_level_errors.py index 6f6e48c..167a434 100644 --- a/test/test_low_level_errors.py +++ b/test/test_low_level_errors.py @@ -22,10 +22,11 @@ from asyncio import get_running_loop, wait_for from typing import Any +from sdbus.dbus_common_elements import DbusLocalObjectMeta +from sdbus.exceptions import DbusFailedError from sdbus.unittest import IsolatedDbusTestCase from sdbus import ( - DbusFailedError, DbusInterfaceCommonAsync, dbus_method_async, dbus_property_async, @@ -48,7 +49,7 @@ class IndependentError(Exception): class InterfaceWithErrors( DbusInterfaceCommonAsync, - interface_name='org.example.test', + interface_name='org.example.errors', ): @dbus_property_async('s') def indep_err_getter(self) -> str: @@ -164,7 +165,10 @@ async def test_property_setter_derived_error(self) -> None: await self.test_object_connection.hello_world() async def test_property_callback_error(self) -> None: - interface = self.test_object._activated_interfaces[0] + dbus_local_meta = self.test_object._dbus + if not isinstance(dbus_local_meta, DbusLocalObjectMeta): + raise TypeError + interface = dbus_local_meta.activated_interfaces[0] interface.property_get_dict.pop(b'DerriveErrSettable') with self.assertRaises(DbusFailedError): @@ -175,7 +179,10 @@ async def test_property_callback_error(self) -> None: async def test_method_callback_error(self) -> None: TEST_KEY = b'HelloWorld' - interface = self.test_object._activated_interfaces[0] + dbus_local_meta = self.test_object._dbus + if not isinstance(dbus_local_meta, DbusLocalObjectMeta): + raise TypeError + interface = dbus_local_meta.activated_interfaces[0] interface.method_dict.pop(TEST_KEY) with self.assertRaises(DbusFailedError): diff --git a/test/test_object_manager.py b/test/test_object_manager.py index 2e881cd..2c01a9a 100644 --- a/test/test_object_manager.py +++ b/test/test_object_manager.py @@ -17,13 +17,15 @@ # 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 __future__ import annotations -from asyncio import get_running_loop, sleep, wait_for -from typing import Any, Dict, List, Tuple - +from sdbus.exceptions import DbusUnknownObjectError from sdbus.unittest import IsolatedDbusTestCase +from sdbus.utils import ( + parse_get_managed_objects, + parse_interfaces_added, + parse_interfaces_removed, +) from sdbus import ( DbusInterfaceCommonAsync, @@ -38,7 +40,7 @@ class ObjectManagerTestInterface( DbusObjectManagerInterfaceAsync, - interface_name='org.test.test', + interface_name='org.test.objectmanager', ): @dbus_method_async( result_signature='s', @@ -68,7 +70,6 @@ def test_int(self) -> int: class TestObjectManager(IsolatedDbusTestCase): async def test_object_manager(self) -> None: - loop = get_running_loop() await self.bus.request_name_async(CONNECTION_NAME, 0) object_manager = ObjectManagerTestInterface() @@ -81,31 +82,14 @@ async def test_object_manager(self) -> None: await object_manager_connection.get_hello_world(), HELLO_WORLD) - async def catch_interfaces_added() -> Tuple[str, - Dict[str, - Dict[str, Any]]]: - async for x in object_manager_connection.interfaces_added: - return x - - raise RuntimeError - - catch_added_task = loop.create_task(catch_interfaces_added()) - - async def catch_interfaces_removed() -> Tuple[str, List[str]]: - async for x in object_manager_connection.interfaces_removed: - return x - - raise RuntimeError - - catch_removed_task = loop.create_task(catch_interfaces_removed()) - - await sleep(0) - managed_object = ManagedInterface() - object_manager.export_with_manager(MANAGED_PATH, managed_object) + async with self.assertDbusSignalEmits( + object_manager_connection.interfaces_added + ) as added_interfaces_catch: + object_manager.export_with_manager(MANAGED_PATH, managed_object) - caught_added = await wait_for(catch_added_task, timeout=0.5) + caught_added = added_interfaces_catch.output[0] added_path, added_attributes = caught_added @@ -118,10 +102,15 @@ async def catch_interfaces_removed() -> Tuple[str, List[str]]: TEST_NUMBER, ) - object_manager.remove_managed_object(managed_object) + with self.subTest("Test interfaces added parser"): + parse_interfaces_added(ManagedInterface, caught_added) + + async with self.assertDbusSignalEmits( + object_manager_connection.interfaces_removed + ) as removed_interfaces_catch: + object_manager.remove_managed_object(managed_object) - path_removed, interfaces_removed = await wait_for( - catch_removed_task, timeout=1) + path_removed, interfaces_removed = removed_interfaces_catch.output[0] self.assertEqual(path_removed, MANAGED_PATH) @@ -138,3 +127,282 @@ def test_expot_with_no_manager(self) -> None: MANAGED_PATH, managed_object, ) + + async def test_parse_interfaces_added_removed(self) -> None: + MANAGED_TWO_INTERFACE_NAME = MANAGED_INTERFACE_NAME + 'Two' + + class ManagedTwoInterface( + ManagedInterface, + interface_name=MANAGED_TWO_INTERFACE_NAME, + ): + + @dbus_property_async('s') + def test_str(self) -> str: + return 'test' + + await self.bus.request_name_async(CONNECTION_NAME, 0) + + object_manager = DbusObjectManagerInterfaceAsync() + object_manager.export_to_dbus(OBJECT_MANAGER_PATH) + + object_manager_connection = DbusObjectManagerInterfaceAsync.new_proxy( + CONNECTION_NAME, OBJECT_MANAGER_PATH) + + managed_object = ManagedTwoInterface() + + async with self.assertDbusSignalEmits( + object_manager_connection.interfaces_added + ) as added_interfaces_catch: + object_manager.export_with_manager(MANAGED_PATH, managed_object) + + caught_added = added_interfaces_catch.output[0] + + with self.subTest('Parse added class'): + path, python_class, python_properties = ( + parse_interfaces_added(ManagedTwoInterface, caught_added) + ) + + self.assertEqual(path, MANAGED_PATH) + self.assertEqual(python_class, ManagedTwoInterface) + self.assertIn('test_str', python_properties) + self.assertIn('test_int', python_properties) + + with self.subTest('Parse added object'): + path, python_class, python_properties = ( + parse_interfaces_added(managed_object, caught_added) + ) + + self.assertEqual(path, MANAGED_PATH) + self.assertEqual(python_class, ManagedTwoInterface) + self.assertIn('test_str', python_properties) + self.assertIn('test_int', python_properties) + + with self.subTest('Parse added iterable'): + path, python_class, python_properties = ( + parse_interfaces_added( + (ManagedInterface, ManagedTwoInterface), + caught_added) + ) + + self.assertEqual(path, MANAGED_PATH) + self.assertEqual(python_class, ManagedTwoInterface) + self.assertIn('test_str', python_properties) + self.assertIn('test_int', python_properties) + + with self.subTest('Parse added unknown'): + with self.assertRaises(KeyError): + path, python_class, python_properties = ( + parse_interfaces_added( + ManagedInterface, + caught_added) + ) + + with self.assertRaises(KeyError): + path, python_class, python_properties = ( + parse_interfaces_added( + ManagedInterface, + caught_added, + on_unknown_interface='none', + ) + ) + + path, python_class, python_properties = ( + parse_interfaces_added( + ManagedInterface, + caught_added, + on_unknown_interface='none', + on_unknown_member='reuse', + ) + ) + + self.assertEqual(path, MANAGED_PATH) + self.assertIsNone(python_class) + self.assertIn('TestStr', python_properties) + self.assertIn('TestInt', python_properties) + + get_managed_data = ( + await object_manager_connection.get_managed_objects() + ) + with self.subTest('Parse get managed objects class'): + managed_dict = ( + parse_get_managed_objects( + ManagedTwoInterface, + get_managed_data, + ) + ) + + self.assertIn(MANAGED_PATH, managed_dict) + managed_class, managed_properties = ( + managed_dict[MANAGED_PATH] + ) + self.assertEqual(managed_class, ManagedTwoInterface) + self.assertIn('test_str', managed_properties) + self.assertIn('test_int', managed_properties) + + with self.subTest('Parse get managed objects object'): + managed_dict = ( + parse_get_managed_objects( + managed_object, + get_managed_data, + ) + ) + + self.assertIn(MANAGED_PATH, managed_dict) + managed_class, managed_properties = ( + managed_dict[MANAGED_PATH] + ) + self.assertEqual(managed_class, ManagedTwoInterface) + self.assertIn('test_str', managed_properties) + self.assertIn('test_int', managed_properties) + + with self.subTest('Parse get managed objects iterable'): + managed_dict = ( + parse_get_managed_objects( + (ManagedInterface, ManagedTwoInterface), + get_managed_data, + ) + ) + + self.assertIn(MANAGED_PATH, managed_dict) + managed_class, managed_properties = ( + managed_dict[MANAGED_PATH] + ) + self.assertEqual(managed_class, ManagedTwoInterface) + self.assertIn('test_str', managed_properties) + self.assertIn('test_int', managed_properties) + + with self.subTest('Parse get managed objects unknown'): + with self.assertRaises(KeyError): + managed_dict = ( + parse_get_managed_objects( + ManagedInterface, + get_managed_data, + ) + ) + + with self.assertRaises(KeyError): + managed_dict = ( + parse_get_managed_objects( + ManagedInterface, + get_managed_data, + on_unknown_interface='none', + ) + ) + + managed_dict = ( + parse_get_managed_objects( + ManagedInterface, + get_managed_data, + on_unknown_interface='none', + on_unknown_member='reuse', + ) + ) + path, python_class, python_properties = ( + parse_interfaces_added( + ManagedInterface, + caught_added, + on_unknown_interface='none', + on_unknown_member='reuse', + ) + ) + + self.assertIn(MANAGED_PATH, managed_dict) + managed_class, managed_properties = ( + managed_dict[MANAGED_PATH] + ) + self.assertIsNone(managed_class) + self.assertIn('TestStr', managed_properties) + self.assertIn('TestInt', managed_properties) + + async with self.assertDbusSignalEmits( + object_manager_connection.interfaces_removed + ) as removed_interfaces_catch: + object_manager.remove_managed_object(managed_object) + + interfaces_removed_data = removed_interfaces_catch.output[0] + + with self.subTest('Parse removed class'): + path, python_class = ( + parse_interfaces_removed( + ManagedTwoInterface, + interfaces_removed_data, + ) + ) + + self.assertEqual(path, MANAGED_PATH) + self.assertEqual(python_class, ManagedTwoInterface) + + with self.subTest('Parse removed unknown'): + with self.assertRaises(KeyError): + path, python_class = ( + parse_interfaces_removed( + ManagedInterface, + interfaces_removed_data, + ) + ) + + path, python_class = ( + parse_interfaces_removed( + ManagedInterface, + interfaces_removed_data, + on_unknown_interface='none', + ) + ) + + self.assertEqual(path, MANAGED_PATH) + self.assertIsNone(python_class) + + async def test_main_export_handle(self) -> None: + await self.bus.request_name_async(CONNECTION_NAME, 0) + + object_manager = ObjectManagerTestInterface() + + object_manager_connection = ObjectManagerTestInterface.new_proxy( + CONNECTION_NAME, OBJECT_MANAGER_PATH) + + with object_manager.export_to_dbus(OBJECT_MANAGER_PATH): + self.assertIsInstance( + await object_manager_connection.get_managed_objects(), + dict, + ) + + with self.assertRaises(DbusUnknownObjectError): + self.assertIsInstance( + await object_manager_connection.get_managed_objects(), + dict, + ) + + async def test_secondary_export_handle(self) -> None: + await self.bus.request_name_async(CONNECTION_NAME, 0) + + object_manager = ObjectManagerTestInterface() + + object_manager_connection = ObjectManagerTestInterface.new_proxy( + CONNECTION_NAME, OBJECT_MANAGER_PATH) + object_manager.export_to_dbus(OBJECT_MANAGER_PATH) + + managed_object = ManagedInterface() + managed_proxy = ManagedInterface.new_proxy( + CONNECTION_NAME, MANAGED_PATH, + ) + + async with self.assertDbusSignalEmits( + object_manager_connection.interfaces_added + ) as added, self.assertDbusSignalEmits( + object_manager_connection.interfaces_removed + ) as removed, object_manager.export_with_manager( + MANAGED_PATH, managed_object, + ): + self.assertEqual( + await managed_proxy.test_int, + TEST_NUMBER, + ) + + self.assertEqual(added.output[0][0], MANAGED_PATH) + + removed_path, removed_interfaces = removed.output[0] + self.assertEqual(removed_path, MANAGED_PATH) + self.assertIn(MANAGED_INTERFACE_NAME, removed_interfaces) + + with self.assertRaises(DbusUnknownObjectError): + await managed_proxy.test_int diff --git a/test/test_read_write_dbus_types.py b/test/test_read_write_dbus_types.py index d3a1a01..cac57ba 100644 --- a/test/test_read_write_dbus_types.py +++ b/test/test_read_write_dbus_types.py @@ -19,7 +19,6 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA from __future__ import annotations -from typing import Dict, List from unittest import main from sdbus.sd_bus_internals import SdBus, SdBusMessage @@ -171,7 +170,7 @@ def test_array(self) -> None: def test_empty_array(self) -> None: message = create_message(self.bus) - test_array: List[str] = [] + test_array: list[str] = [] message.append_data("as", test_array) message.seal() @@ -234,7 +233,7 @@ def test_dict(self) -> None: def test_empty_dict(self) -> None: message = create_message(self.bus) - test_dict: Dict[str, str] = {} + test_dict: dict[str, str] = {} message.append_data("a{ss}", test_dict) message.seal() @@ -429,6 +428,17 @@ class TestEnum(str, Enum): self.assertEqual(message.get_contents(), TestEnum.SOMETHING) + def test_reading_multiple_times(self) -> None: + message = create_message(self.bus) + message.append_data('s', 'test') + message.seal() + + for _ in range(5): + self.assertEqual( + message.get_contents(), + "test", + ) + if __name__ == "__main__": main() diff --git a/test/test_request_name.py b/test/test_request_name.py new file mode 100644 index 0000000..e221831 --- /dev/null +++ b/test/test_request_name.py @@ -0,0 +1,227 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later + +# Copyright (C) 2023 igo95862 + +# This file is part of python-sdbus + +# 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 __future__ import annotations + +from asyncio import get_running_loop, sleep, wait_for +from unittest import main + +from sdbus.exceptions import ( + SdBusLibraryError, + SdBusRequestNameAlreadyOwnerError, + SdBusRequestNameError, + SdBusRequestNameExistsError, + SdBusRequestNameInQueueError, +) +from sdbus.sd_bus_internals import NameAllowReplacementFlag, NameQueueFlag +from sdbus.unittest import IsolatedDbusTestCase +from sdbus_async.dbus_daemon import FreedesktopDbus + +from sdbus import ( + request_default_bus_name, + request_default_bus_name_async, + sd_bus_open_user, +) + +TEST_BUS_NAME = 'com.example.test' +TEST_BUS_NAME_regex_match = TEST_BUS_NAME.replace('.', r'\.') + + +class TestRequestNameLowLevel(IsolatedDbusTestCase): + def test_request_name_exception_tree(self) -> None: + # Test that SdBusRequestNameError is super class + # of other request name exceptions + self.assertTrue( + issubclass( + SdBusRequestNameAlreadyOwnerError, + SdBusRequestNameError, + ) + ) + self.assertTrue( + issubclass( + SdBusRequestNameExistsError, + SdBusRequestNameError, + ) + ) + self.assertTrue( + issubclass( + SdBusRequestNameInQueueError, + SdBusRequestNameError, + ) + ) + # Test the opposite + self.assertFalse( + issubclass( + SdBusRequestNameAlreadyOwnerError, + SdBusRequestNameExistsError, + ) + ) + self.assertFalse( + issubclass( + SdBusRequestNameInQueueError, + SdBusRequestNameExistsError, + ) + ) + self.assertFalse( + issubclass( + SdBusRequestNameInQueueError, + SdBusRequestNameAlreadyOwnerError, + ) + ) + + async def test_name_exists_async(self) -> None: + extra_bus = sd_bus_open_user() + await self.bus.request_name_async(TEST_BUS_NAME, 0) + + with self.assertRaises(SdBusRequestNameExistsError): + await wait_for( + extra_bus.request_name_async(TEST_BUS_NAME, 0), + timeout=1, + ) + + async def test_name_already_async(self) -> None: + await self.bus.request_name_async(TEST_BUS_NAME, 0) + + with self.assertRaises(SdBusRequestNameAlreadyOwnerError): + await wait_for( + self.bus.request_name_async(TEST_BUS_NAME, 0), + timeout=1, + ) + + async def test_name_queued_async(self) -> None: + extra_bus = sd_bus_open_user() + await self.bus.request_name_async(TEST_BUS_NAME, 0) + + with self.assertRaises(SdBusRequestNameInQueueError): + await wait_for( + extra_bus.request_name_async(TEST_BUS_NAME, NameQueueFlag), + timeout=1, + ) + + async def test_name_other_error_async(self) -> None: + extra_bus = sd_bus_open_user() + extra_bus.close() + + with self.assertRaises(SdBusLibraryError): + await wait_for( + extra_bus.request_name_async(TEST_BUS_NAME, 0), + timeout=1, + ) + + def test_name_exists_block(self) -> None: + extra_bus = sd_bus_open_user() + self.bus.request_name(TEST_BUS_NAME, 0) + + with self.assertRaisesRegex( + SdBusRequestNameExistsError, + TEST_BUS_NAME_regex_match, + ): + extra_bus.request_name(TEST_BUS_NAME, 0) + + def test_name_already_block(self) -> None: + self.bus.request_name(TEST_BUS_NAME, 0) + + with self.assertRaisesRegex( + SdBusRequestNameAlreadyOwnerError, + TEST_BUS_NAME_regex_match, + ): + self.bus.request_name(TEST_BUS_NAME, 0) + + def test_name_queued_block(self) -> None: + extra_bus = sd_bus_open_user() + self.bus.request_name(TEST_BUS_NAME, 0) + + with self.assertRaisesRegex( + SdBusRequestNameInQueueError, + TEST_BUS_NAME_regex_match, + ): + extra_bus.request_name(TEST_BUS_NAME, NameQueueFlag) + + def test_name_other_error_block(self) -> None: + extra_bus = sd_bus_open_user() + extra_bus.close() + with self.assertRaises(SdBusLibraryError): + extra_bus.request_name(TEST_BUS_NAME, 0) + + +class TestRequestNameBlock(IsolatedDbusTestCase): + def test_request_name_replacement(self) -> None: + extra_bus = sd_bus_open_user() + extra_bus.request_name(TEST_BUS_NAME, NameAllowReplacementFlag) + + with self.assertRaises(SdBusRequestNameExistsError): + request_default_bus_name(TEST_BUS_NAME) + + request_default_bus_name( + TEST_BUS_NAME, + replace_existing=True, + ) + + +class TestRequestNameAsync(IsolatedDbusTestCase): + async def test_request_name_replacement(self) -> None: + extra_bus = sd_bus_open_user() + await extra_bus.request_name_async( + TEST_BUS_NAME, + NameAllowReplacementFlag, + ) + + with self.assertRaises(SdBusRequestNameExistsError): + await request_default_bus_name_async(TEST_BUS_NAME) + + await request_default_bus_name_async( + TEST_BUS_NAME, + replace_existing=True, + ) + + async def test_request_name_queue(self) -> None: + extra_bus = sd_bus_open_user() + await extra_bus.request_name_async(TEST_BUS_NAME, 0) + + with self.assertRaises(SdBusRequestNameInQueueError): + await request_default_bus_name_async( + TEST_BUS_NAME, + queue=True, + ) + + async def catch_owner_changed() -> str: + dbus = FreedesktopDbus() + async for name, old, new in dbus.name_owner_changed: + if name != TEST_BUS_NAME: + continue + + if old and new: + return new + + raise RuntimeError + + loop = get_running_loop() + owner_changed_task = loop.create_task(catch_owner_changed()) + await sleep(0) + + extra_bus.close() + + await wait_for(owner_changed_task, timeout=0.5) + + with self.assertRaises(SdBusRequestNameAlreadyOwnerError): + await request_default_bus_name_async(TEST_BUS_NAME) + + +if __name__ == '__main__': + main() diff --git a/test/test_sd_bus_async.py b/test/test_sdbus_async.py similarity index 57% rename from test/test_sd_bus_async.py rename to test/test_sdbus_async.py index 8cd5699..4ce7723 100644 --- a/test/test_sd_bus_async.py +++ b/test/test_sdbus_async.py @@ -1,6 +1,6 @@ # SPDX-License-Identifier: LGPL-2.1-or-later -# Copyright (C) 2020, 2021 igo95862 +# Copyright (C) 2020-2023 igo95862 # This file is part of python-sdbus @@ -17,32 +17,34 @@ # 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 __future__ import annotations -from asyncio import Event, get_running_loop, sleep, wait_for +from asyncio import Event, get_running_loop +from asyncio import run as asyncio_run +from asyncio import sleep, wait_for from asyncio.subprocess import create_subprocess_exec -from typing import Tuple +from typing import TYPE_CHECKING from unittest import SkipTest -from sdbus.dbus_common_funcs import PROPERTY_FLAGS_MASK, count_bits +from sdbus.exceptions import ( + DbusFailedError, + DbusFileExistsError, + DbusNoReplyError, + DbusPropertyReadOnlyError, + DbusUnknownObjectError, + SdBusLibraryError, + SdBusUnmappedMessageError, +) from sdbus.sd_bus_internals import ( DBUS_ERROR_TO_EXCEPTION, - DbusDeprecatedFlag, - DbusPropertyConstFlag, DbusPropertyEmitsChangeFlag, - is_interface_name_valid, ) from sdbus.unittest import IsolatedDbusTestCase +from sdbus.utils.parse import parse_properties_changed from sdbus import ( - DbusFailedError, - DbusFileExistsError, DbusInterfaceCommonAsync, DbusNoReplyFlag, - DbusUnknownObjectError, - SdBusLibraryError, - SdBusUnmappedMessageError, dbus_method_async, dbus_method_async_override, dbus_property_async, @@ -51,6 +53,13 @@ get_current_message, ) +if TYPE_CHECKING: + from sdbus.dbus_proxy_async_interfaces import ( + DBUS_PROPERTIES_CHANGED_TYPING, + ) +else: + DBUS_PROPERTIES_CHANGED_TYPING = None + class TestPing(IsolatedDbusTestCase): @@ -83,15 +92,20 @@ async def test_request_name(self) -> None: await self.bus.request_name_async("org.example.test", 0) -class TestInterface(DbusInterfaceCommonAsync, - interface_name='org.test.test', - ): +TEST_INTERFACE_NAME = "org.test.test" + + +class TestInterface( + DbusInterfaceCommonAsync, + interface_name=TEST_INTERFACE_NAME, +): def __init__(self) -> None: super().__init__() self.test_string = 'test_property' self.test_string_read = 'read' self.test_no_reply_string = 'no' + self.property_private = 100 self.no_reply_sync = Event() @dbus_method_async("s", "s") @@ -128,6 +142,14 @@ def test_property_set(self, new_property: str) -> None: def test_property_read_only(self) -> str: return self.test_string_read + @dbus_property_async("x") + def test_property_private(self) -> int: + return self.property_private + + @test_property_private.setter_private + def test_private_setter(self, new_value: int) -> None: + self.property_private = new_value + @dbus_method_async("sb", "s") async def kwargs_function( self, @@ -149,7 +171,7 @@ async def kwargs_function_annotated( return input.lower() @dbus_signal_async('ss') - def test_signal(self) -> Tuple[str, str]: + def test_signal(self) -> tuple[str, str]: """Test signal""" raise NotImplementedError @@ -174,6 +196,10 @@ async def raise_and_unmap_error(self) -> None: raise DbusErrorUnmappedLater('Should be unmapped') + @dbus_method_async() + async def raise_python_exc(self) -> None: + raise ValueError("Test!") + @dbus_method_async('s', flags=DbusNoReplyFlag) async def no_reply_method(self, new_value: str) -> None: self.no_reply_sync.set() @@ -185,19 +211,42 @@ def test_constant_property(self) -> str: @dbus_method_async( result_signature='(ss)' ) - async def test_struct_return(self) -> Tuple[str, str]: + async def test_struct_return(self) -> tuple[str, str]: return ('hello', 'world') @dbus_method_async( result_signature='(ss)' ) - async def test_struct_return_workaround(self) -> Tuple[Tuple[str, str]]: + async def test_struct_return_workaround(self) -> tuple[tuple[str, str]]: return (('hello', 'world'), ) @dbus_method_async() async def looong_method(self) -> None: await sleep(100) + @dbus_signal_async() + def empty_signal(self) -> None: + raise NotImplementedError + + @dbus_method_async() + async def returns_none_method(self) -> None: + return + + @dbus_method_async( + input_signature="(iiii)", + result_signature="i", + ) + async def takes_struct_method( + self, + int_struct: tuple[int, int, int, int], + ) -> int: + a, b, c, d = int_struct + return a*b*c*d + + @dbus_method_async("s", "x") + async def return_length(self, input_str: str) -> int: + return len(input_str) + class DbusErrorTest(DbusFailedError): dbus_error_name = 'org.example.Error' @@ -210,7 +259,7 @@ class DbusErrorUnmappedLater(DbusFailedError): TEST_SERVICE_NAME = 'org.example.test' -def initialize_object() -> Tuple[TestInterface, TestInterface]: +def initialize_object() -> tuple[TestInterface, TestInterface]: test_object = TestInterface() test_object.export_to_dbus('/') @@ -289,6 +338,26 @@ async def test_method(self) -> None: self.assertTrue(await test_object_connection.get_sender()) + with self.subTest("Test method that returns None"): + self.assertIsNone( + await test_object + .returns_none_method() # type: ignore[func-returns-value] + ) + self.assertIsNone( + await test_object_connection + .returns_none_method() # type: ignore[func-returns-value] + ) + + with self.subTest("Test method that takes a single struct"): + self.assertEqual( + await test_object.takes_struct_method((2, 3, 4, 5)), + 120, + ) + self.assertEqual( + await test_object_connection.takes_struct_method((9, 8, 7, 6)), + 3024, + ) + async def test_subclass(self) -> None: test_object, test_object_connection = initialize_object() @@ -305,7 +374,6 @@ def test_property(self) -> str: @test_property.setter def test_property_setter(self, var: str) -> None: - nonlocal test_var test_var.insert(0, var) test_subclass = TestInheritence() @@ -327,19 +395,26 @@ def test_property_setter(self, var: str) -> None: self.assertEqual('12345', await test_subclass.test_property) with self.subTest('Test dbus to python mapping'): + dbus_elements_map = ( + { + interface_name: meta.dbus_member_to_python_attr + for interface_name, meta in + TestInterface._dbus_iter_interfaces_meta() + } + ) self.assertIn( - test_object.properties_changed.dbus_signal.signal_name, - test_object._dbus_to_python_name_map, + "TestInt", + dbus_elements_map[TEST_INTERFACE_NAME], ) self.assertIn( - test_subclass.properties_changed.dbus_signal.signal_name, - test_subclass._dbus_to_python_name_map, + "TestInt", + dbus_elements_map[TEST_INTERFACE_NAME], ) self.assertIn( - test_subclass.test_property.dbus_property.property_name, - test_subclass._dbus_to_python_name_map, + "TestProperty", + dbus_elements_map[TEST_INTERFACE_NAME], ) with self.subTest('Tripple subclass'): @@ -367,18 +442,6 @@ def test_property(self) -> str: self.assertEqual( await test_subclass_tri_connection.test_property, 'tri') - async def test_bad_subclass(self) -> None: - with self.assertRaises(TypeError): - class TestInheritence(TestInterface): - async def test_int(self) -> int: - return 2 - - with self.assertRaises(TypeError): - class TestInheritence2(TestInterface): - @dbus_method_async_override() - async def test_unrelated(self) -> int: - return 2 - async def test_properties(self) -> None: test_object, test_object_connection = initialize_object() @@ -420,19 +483,24 @@ async def test_properties(self) -> None: async def test_signal(self) -> None: test_object, test_object_connection = initialize_object() - loop = get_running_loop() - test_tuple = ('sgfsretg', 'asd') - ai_dbus = test_object_connection.test_signal.__aiter__() - aw_dbus = ai_dbus.__anext__() - q = test_object.test_signal._get_local_queue() - - loop.call_at(0, test_object.test_signal.emit, test_tuple) + async with self.assertDbusSignalEmits( + test_object.test_signal + ) as local_signals_record, self.assertDbusSignalEmits( + test_object_connection.test_signal + ) as remote_signals_record: + test_object.test_signal.emit(test_tuple) - self.assertEqual(test_tuple, await wait_for(aw_dbus, timeout=1)) + async with self.assertDbusSignalEmits( + test_object.test_signal + ) as local_signals_record, self.assertDbusSignalEmits( + test_object_connection.test_signal + ) as remote_signals_record: + test_object.test_signal.emit(test_tuple) - self.assertEqual(test_tuple, await wait_for(q.get(), timeout=1)) + self.assertEqual([test_tuple], local_signals_record.output) + self.assertEqual([test_tuple], remote_signals_record.output) async def test_signal_catch_anywhere(self) -> None: test_object, test_object_connection = initialize_object() @@ -443,7 +511,7 @@ async def test_signal_catch_anywhere(self) -> None: with self.subTest('Catch anywhere over D-Bus object'): async def catch_anywhere_oneshot_dbus( - ) -> Tuple[str, Tuple[str, str]]: + ) -> tuple[str, tuple[str, str]]: async for x in test_object_connection.test_signal\ .catch_anywhere(): return x @@ -464,7 +532,7 @@ async def catch_anywhere_oneshot_dbus( with self.subTest('Catch anywhere over D-Bus class'): async def catch_anywhere_oneshot_from_class( - ) -> Tuple[str, Tuple[str, str]]: + ) -> tuple[str, tuple[str, str]]: async for x in TestInterface.test_signal.catch_anywhere( TEST_SERVICE_NAME, self.bus): return x @@ -485,7 +553,7 @@ async def catch_anywhere_oneshot_from_class( with self.subTest('Catch anywhere over local object'): async def catch_anywhere_oneshot_local( - ) -> Tuple[str, Tuple[str, str]]: + ) -> tuple[str, tuple[str, str]]: async for x in test_object.test_signal.catch_anywhere(): return x @@ -500,6 +568,33 @@ async def catch_anywhere_oneshot_local( timeout=1, ) + async def test_signal_multiple_readers(self) -> None: + test_object, test_object_connection = initialize_object() + + loop = get_running_loop() + + test_tuple = ('sgfsretg', 'asd') + + async def reader_one() -> tuple[str, str]: + async for x in test_object_connection.test_signal.catch(): + return test_tuple + + raise RuntimeError + + async def reader_two() -> tuple[str, str]: + async for x in test_object_connection.test_signal.catch(): + return test_tuple + + raise RuntimeError + + t1 = loop.create_task(reader_one()) + t2 = loop.create_task(reader_two()) + + loop.call_at(0, test_object.test_signal.emit, test_tuple) + + self.assertEqual(test_tuple, await wait_for(t1, timeout=1)) + self.assertEqual(test_tuple, await wait_for(t2, timeout=1)) + async def test_exceptions(self) -> None: test_object, test_object_connection = initialize_object() @@ -617,51 +712,6 @@ async def catch_property_emit_local() -> str: self.assertEqual(t1_result, test_str) self.assertEqual(t2_result, test_str) - async def test_property_flags(self) -> None: - self.assertEqual(0, PROPERTY_FLAGS_MASK & DbusDeprecatedFlag) - self.assertEqual( - 1, - count_bits(PROPERTY_FLAGS_MASK & (DbusDeprecatedFlag - | DbusPropertyEmitsChangeFlag)) - ) - self.assertEqual( - 2, - count_bits( - PROPERTY_FLAGS_MASK & ( - DbusDeprecatedFlag | - DbusPropertyConstFlag | - DbusPropertyEmitsChangeFlag))) - - def must_raise_value_error() -> None: - class InvalidPropertiesFlags( - DbusInterfaceCommonAsync, - interface_name='org.test.test'): - @dbus_property_async( - "s", - flags=DbusPropertyConstFlag | DbusPropertyEmitsChangeFlag, - ) - def test_constant(self) -> str: - return "a" - - self.assertRaisesRegex( - AssertionError, - '^Incorrect number of Property flags', - must_raise_value_error, - ) - - def should_be_no_error() -> None: - class ValidPropertiesFlags( - DbusInterfaceCommonAsync, - interface_name='org.test.test'): - @dbus_property_async( - "s", - flags=DbusDeprecatedFlag | DbusPropertyEmitsChangeFlag, - ) - def test_constant(self) -> str: - return "a" - - should_be_no_error() - async def test_bus_close(self) -> None: test_object, test_object_connection = initialize_object() @@ -673,18 +723,39 @@ async def too_long_wait() -> None: with self.assertRaises(SdBusLibraryError): await wait_for(too_long_wait(), timeout=1) - async def test_singal_queue_wildcard_match(self) -> None: + async def test_bus_timerfd(self) -> None: test_object, test_object_connection = initialize_object() - message_queue = await self.bus.get_signal_queue_async( + self.bus.method_call_timeout_usec = 10_000 # 0.01 seconds + + loop = get_running_loop() + + start = loop.time() + + with self.assertRaises(DbusNoReplyError): + await wait_for(test_object_connection.looong_method(), timeout=1) + + self.assertLess(loop.time() - start, 0.2) + + async def test_signal_queue_wildcard_match(self) -> None: + test_object, test_object_connection = initialize_object() + + loop = get_running_loop() + future = loop.create_future() + + slot = await self.bus.match_signal_async( TEST_SERVICE_NAME, - None, None, None) + None, None, None, + future.set_result) - test_object.test_signal.emit(('test', 'signal')) + try: + test_object.test_signal.emit(('test', 'signal')) - message = await wait_for(message_queue.get(), timeout=1) - self.assertEqual(message.member, - test_object.test_signal.dbus_signal.signal_name) + await wait_for(future, timeout=1) + message = future.result() + self.assertEqual(message.member, "TestSignal") + finally: + slot.close() async def test_class_with_string_subclass_parameter(self) -> None: from enum import Enum @@ -709,96 +780,289 @@ def hello_world(self) -> str: test_object = EnumedInterfaceAsync() test_object.export_to_dbus(ObjectPathEnum.FOO) - async def test_name_validations(self) -> None: - if not __debug__: - raise SkipTest('Assertions are not enabled') + async def test_properties_get_all_dict(self) -> None: + test_object, test_object_connection = initialize_object() - try: - is_interface_name_valid('org.test') - except NotImplementedError: - raise SkipTest('Validation functions not available') - - def test_bad_interface_name() -> None: - class BadInterfaceName( - DbusInterfaceCommonAsync, - interface_name='0.test', - ): - ... + dbus_dict = await test_object_connection._properties_get_all( + TEST_INTERFACE_NAME) - self.assertRaisesRegex( - AssertionError, - '^Invalid interface name', - test_bad_interface_name, + self.assertEqual( + await test_object.test_property, + dbus_dict['TestProperty'][1], ) - def test_bad_method_name() -> None: - class BadMethodName( - DbusInterfaceCommonAsync, - interface_name='org.example', - ): - @dbus_method_async( - result_signature='s', - method_name='🤫', - ) - async def test(self) -> str: - return 'test' - - self.assertRaisesRegex( - AssertionError, - '^Invalid method name', - test_bad_method_name, + self.assertEqual( + await test_object.test_property, + ( + await test_object_connection.properties_get_all_dict() + )['test_property'], ) - def test_bad_property_name() -> None: - class BadPropertyName( - DbusInterfaceCommonAsync, - interface_name='org.example', - ): - @dbus_property_async( - property_signature='s', - property_name='🤫', - ) - def test(self) -> str: - return 'test' + async def test_empty_signal(self) -> None: + test_object, test_object_connection = initialize_object() + + async with self.assertDbusSignalEmits( + test_object.empty_signal + ) as local_signals_record, self.assertDbusSignalEmits( + test_object_connection.empty_signal + ) as remote_signals_record: + test_object.empty_signal.emit(None) + + self.assertEqual([None], local_signals_record.output) + self.assertEqual([None], remote_signals_record.output) + + async def test_properties_changed(self) -> None: + test_object, test_object_connection = initialize_object() + + test_str = 'should_be_emited' - self.assertRaisesRegex( - AssertionError, - '^Invalid property name', - test_bad_property_name, + async with self.assertDbusSignalEmits( + test_object_connection.properties_changed + ) as properties_changed_catch: + await test_object_connection.test_property.set_async(test_str) + + properties_changed_data = properties_changed_catch.output[0] + + parsed_dict_from_class = parse_properties_changed( + TestInterface, properties_changed_data) + self.assertEqual( + test_str, + parsed_dict_from_class['test_property'], ) - def test_bad_signal_name() -> None: - class BadSignalName( - DbusInterfaceCommonAsync, - interface_name='org.example', - ): - @dbus_signal_async( - signal_signature='s', - signal_name='🤫', - ) - def test(self) -> str: - raise NotImplementedError + parsed_dict_from_object = parse_properties_changed( + test_object_connection, properties_changed_data) + self.assertEqual( + test_str, + parsed_dict_from_object['test_property'], + ) - self.assertRaisesRegex( - AssertionError, - '^Invalid signal name', - test_bad_signal_name, + properties_changed_data[2].append('invalidated_property') + parsed_dict_with_invalidation = parse_properties_changed( + test_object, properties_changed_data, + on_unknown_member='reuse', ) + self.assertIsNone( + parsed_dict_with_invalidation['invalidated_property']) - async def test_properties_get_all_dict(self) -> None: + async def test_property_private_setter(self) -> None: test_object, test_object_connection = initialize_object() - dbus_dict = await test_object_connection._properties_get_all( - 'org.test.test') + new_value = 200 + self.assertNotEqual( + await test_object_connection.test_property_private, + new_value + ) + + with self.assertRaises(DbusPropertyReadOnlyError): + await test_object_connection.test_property_private.set_async( + new_value) + + async with self.assertDbusSignalEmits( + test_object_connection.properties_changed + ) as properties_changed_catch: + await test_object.test_property_private.set_async(new_value) + + changed_properties = properties_changed_catch.output[0] self.assertEqual( - await test_object.test_property, - dbus_dict['TestProperty'][1], + await test_object_connection.test_property_private, + new_value + ) + + self.assertIn('TestPropertyPrivate', changed_properties[1]) + + async def test_property_override_setter_private(self) -> None: + + test_int = 1 + + class TestInterfacePrivateSetter(TestInterface): + @dbus_property_async_override() + def test_property_private(self) -> int: + return test_int + + @test_property_private.setter_private + def _private_setter(self, new_value: int) -> None: + nonlocal test_int + test_int = new_value + + test_object = TestInterfacePrivateSetter() + test_object.export_to_dbus('/') + test_object_connection = TestInterface.new_proxy( + TEST_SERVICE_NAME, '/') + + self.assertEqual( + await test_object_connection.test_property_private, + test_int, ) + async def catch_properties_changed() -> int: + async for x in test_object_connection.properties_changed: + changed_attr = parse_properties_changed( + TestInterface, x)["test_property_private"] + + if not isinstance(changed_attr, int): + raise TypeError + + return changed_attr + + raise RuntimeError + + catch_changed_task = get_running_loop( + ).create_task(catch_properties_changed()) + + with self.assertRaises(DbusPropertyReadOnlyError): + await test_object_connection.test_property_private.set_async(10) + + await test_object.test_property_private.set_async(10) + self.assertEqual( - await test_object.test_property, - ( - await test_object_connection.properties_get_all_dict() - )['test_property'], + await test_object_connection.test_property_private, + 10, + ) + + self.assertEqual( + await test_object.test_property_private, + test_int, + ) + + self.assertEqual( + await wait_for(catch_changed_task, timeout=1), + 10, ) + + async def test_interface_composition(self) -> None: + class OneInterface( + DbusInterfaceCommonAsync, + interface_name="org.example.one", + ): + @dbus_method_async(result_signature="x") + async def one(self) -> int: + raise NotImplementedError + + class TwoInterface( + DbusInterfaceCommonAsync, + interface_name="org.example.two", + ): + @dbus_method_async(result_signature="t") + async def two(self) -> int: + return 2 + + class CombinedInterface(TwoInterface, OneInterface): + ... + + test_combined = CombinedInterface() + test_combined_interfaces = [ + iface for iface, _ in test_combined._dbus_iter_interfaces_meta() + ] + + # Verify the order of reported interfaces on the combined class. + self.assertEqual(test_combined_interfaces, [ + "org.freedesktop.DBus.Peer", + "org.freedesktop.DBus.Introspectable", + "org.freedesktop.DBus.Properties", + "org.example.one", + "org.example.two", + ]) + + async def test_extremely_large_string(self) -> None: + test_object, test_object_connection = initialize_object() + + extremely_large_string = "a" * 8423681 + + remote_len = await wait_for( + test_object_connection.return_length( + extremely_large_string + ), + timeout=10, + ) + + self.assertEqual( + remote_len, + len(extremely_large_string), + ) + + # Check that calling regular methods still works. + for _ in range(5): + await test_object_connection.returns_none_method() + + async def test_export_handle(self) -> None: + test_object = TestInterface() + test_object_connection = TestInterface.new_proxy( + TEST_SERVICE_NAME, '/', + ) + with self.assertRaises(DbusUnknownObjectError): + await test_object_connection.returns_none_method() + + with test_object.export_to_dbus("/"): + await test_object_connection.returns_none_method() + + with self.assertRaises(DbusUnknownObjectError): + await test_object_connection.returns_none_method() + + test_object2 = TestInterface() + handle = test_object2.export_to_dbus("/") + await test_object_connection.returns_none_method() + handle.stop() + + with self.assertRaises(DbusUnknownObjectError): + await test_object_connection.returns_none_method() + + async def test_export_handle_lifetime(self) -> None: + test_object = TestInterface() + test_object_connection = TestInterface.new_proxy( + TEST_SERVICE_NAME, '/', + ) + handle = test_object.export_to_dbus("/") + await test_object_connection.returns_none_method() + + del test_object + + handle.stop() + + with self.assertRaises(DbusUnknownObjectError): + await test_object_connection.returns_none_method() + + # Test idempotency + handle.stop() + handle.stop() + + with self.assertRaises(DbusUnknownObjectError): + await test_object_connection.returns_none_method() + + def test_asyncio_run_different_loops(self) -> None: + bus = self.bus + + async def test() -> None: + dbus_object = DbusInterfaceCommonAsync.new_proxy( + "org.freedesktop.DBus", + "/org/freedesktop/DBus", + bus, + ) + await wait_for(dbus_object.dbus_ping(), timeout=1) + + with self.assertRaisesRegex(RuntimeError, "different loop"): + asyncio_run(test()) + + async def test_python_exc(self) -> None: + test_object, test_object_connection = initialize_object() + + with self.assertRaisesRegex(ValueError, "Test!"): + await test_object.raise_python_exc() + + with self.assertRaisesRegex(ValueError, "Test!"): + await test_object_connection.raise_python_exc() + + async def test_empty_dbus_interface(self) -> None: + class Empty( + DbusInterfaceCommonAsync, + interface_name="org.empty", + ): + ... + + empty_local = Empty() + empty_local.export_to_dbus("/") + empty_proxy = Empty.new_proxy(TEST_SERVICE_NAME, "/") + + intro = await empty_proxy.dbus_introspect() + self.assertIn('', intro) diff --git a/test/test_sdbus_async_bad_class.py b/test/test_sdbus_async_bad_class.py new file mode 100644 index 0000000..92e2ee0 --- /dev/null +++ b/test/test_sdbus_async_bad_class.py @@ -0,0 +1,250 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later + +# Copyright (C) 2023 igo95862 + +# This file is part of python-sdbus + +# 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 __future__ import annotations + +from gc import collect +from unittest import TestCase +from unittest import main as unittest_main + +from sdbus.dbus_common_funcs import PROPERTY_FLAGS_MASK, count_bits + +from sdbus import ( + DbusDeprecatedFlag, + DbusInterfaceCommonAsync, + DbusPropertyConstFlag, + DbusPropertyEmitsChangeFlag, + dbus_method_async, + dbus_method_async_override, + dbus_property_async, + dbus_signal_async, +) + +from .common_test_util import skip_if_no_asserts, skip_if_no_name_validations + + +class TestInterface( + DbusInterfaceCommonAsync, + interface_name="org.example.good", +): + @dbus_method_async(result_signature="i") + async def test_int(self) -> int: + return 1 + + +class TestBadAsyncDbusClass(TestCase): + def test_name_validations(self) -> None: + skip_if_no_name_validations() + + with self.assertRaisesRegex( + AssertionError, + "^Invalid interface name", + ): + + class BadInterfaceName( + DbusInterfaceCommonAsync, + interface_name="0.test", + ): + ... + + with self.assertRaisesRegex( + AssertionError, + "^Invalid method name", + ): + + class BadMethodName( + DbusInterfaceCommonAsync, + interface_name="org.example", + ): + @dbus_method_async( + result_signature="s", + method_name="🤫", + ) + async def test(self) -> str: + return "test" + + with self.assertRaisesRegex( + AssertionError, + "^Invalid property name", + ): + + class BadPropertyName( + DbusInterfaceCommonAsync, + interface_name="org.example", + ): + @dbus_property_async( + property_signature="s", + property_name="🤫", + ) + def test(self) -> str: + return "test" + + with self.assertRaisesRegex( + AssertionError, + "^Invalid signal name", + ): + + class BadSignalName( + DbusInterfaceCommonAsync, + interface_name="org.example", + ): + @dbus_signal_async( + signal_signature="s", + signal_name="🤫", + ) + def test(self) -> str: + raise NotImplementedError + + def test_property_flags(self) -> None: + self.assertEqual(0, PROPERTY_FLAGS_MASK & DbusDeprecatedFlag) + self.assertEqual( + 1, + count_bits( + PROPERTY_FLAGS_MASK + & (DbusDeprecatedFlag | DbusPropertyEmitsChangeFlag) + ), + ) + self.assertEqual( + 2, + count_bits( + PROPERTY_FLAGS_MASK + & ( + DbusDeprecatedFlag + | DbusPropertyConstFlag + | DbusPropertyEmitsChangeFlag + ) + ), + ) + + with self.subTest("Test incorrect flags"), self.assertRaisesRegex( + AssertionError, + "^Incorrect number of Property flags", + ): + skip_if_no_asserts() + + class InvalidPropertiesFlags( + DbusInterfaceCommonAsync, interface_name="org.test.invalidprop" + ): + @dbus_property_async( + "s", + flags=DbusPropertyConstFlag | DbusPropertyEmitsChangeFlag, + ) + def test_constant(self) -> str: + return "a" + + with self.subTest("Valid properties flags"): + + class ValidPropertiesFlags( + DbusInterfaceCommonAsync, interface_name="org.test.validprop" + ): + @dbus_property_async( + "s", + flags=DbusDeprecatedFlag | DbusPropertyEmitsChangeFlag, + ) + def test_constant(self) -> str: + return "a" + + def test_bad_subclass(self) -> None: + with self.assertRaises(ValueError): + + class TestInheritence(TestInterface): + async def test_int(self) -> int: + return 2 + + with self.assertRaises(ValueError): + + class TestInheritence2(TestInterface): + @dbus_method_async_override() + async def test_unrelated(self) -> int: + return 2 + + def test_dbus_elements_without_interface_name(self) -> None: + with self.assertRaisesRegex(TypeError, "without interface name"): + + class NoInterfaceName(DbusInterfaceCommonAsync): + @dbus_method_async() + async def example(self) -> None: + ... + + def test_dbus_elements_without_interface_name_subclass(self) -> None: + with self.assertRaisesRegex(TypeError, "without interface name"): + + class NoInterfaceName(TestInterface): + @dbus_method_async() + async def example(self) -> None: + ... + + def test_shared_parent_class(self) -> None: + class One(TestInterface): + ... + + class Two(TestInterface): + ... + + class Shared(One, Two): + ... + + def test_combined_collision(self) -> None: + + class One( + DbusInterfaceCommonAsync, + interface_name="org.example.foo", + ): + @dbus_method_async() + async def example(self) -> None: + ... + + class Two( + DbusInterfaceCommonAsync, + interface_name="org.example.bar", + ): + @dbus_method_async() + async def example(self) -> None: + ... + + with self.assertRaisesRegex(ValueError, "collision"): + class Combined(One, Two): + ... + + def test_class_cleanup(self) -> None: + class One( + DbusInterfaceCommonAsync, + interface_name="org.example.foo1", + ): + ... + + with self.assertRaises(ValueError): + class Two( + DbusInterfaceCommonAsync, + interface_name="org.example.foo1", + ): + ... + + del One + collect() # Let weak refs be processed + + class After( + DbusInterfaceCommonAsync, + interface_name="org.example.foo1", + ): + ... + + +if __name__ == "__main__": + unittest_main() diff --git a/test/test_sdbus_async_introspection.py b/test/test_sdbus_async_introspection.py new file mode 100644 index 0000000..fe17b7e --- /dev/null +++ b/test/test_sdbus_async_introspection.py @@ -0,0 +1,139 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later + +# Copyright (C) 2023 igo95862 + +# This file is part of python-sdbus + +# 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 __future__ import annotations + +from sdbus.unittest import IsolatedDbusTestCase + +from sdbus import DbusInterfaceCommonAsync, dbus_method_async + +TEST_SERVICE_NAME = 'org.example.test' + + +def initialize_object( + interface_class: type[DbusInterfaceCommonAsync], +) -> tuple[DbusInterfaceCommonAsync, DbusInterfaceCommonAsync]: + test_object = interface_class() + test_object.export_to_dbus('/') + + test_object_connection = interface_class.new_proxy( + TEST_SERVICE_NAME, '/') + + return test_object, test_object_connection + + +class TestIntrospection(IsolatedDbusTestCase): + + async def asyncSetUp(self) -> None: + await super().asyncSetUp() + await self.bus.request_name_async("org.example.test", 0) + + async def test_method_arg_names_none(self) -> None: + class TestInterface( + DbusInterfaceCommonAsync, + interface_name="org.test.intro1", + ): + @dbus_method_async( + input_signature="ss", + result_signature="i", + ) + async def login( + self, + user_name: str, + pin_code: str, + ) -> int: + return 0 + + obj, rem = initialize_object(TestInterface) + + introspection = await rem.dbus_introspect() + self.assertNotIn('name="user_name"', introspection) + self.assertNotIn('name="result"', introspection) + self.assertNotIn('name="pin_code"', introspection) + + async def test_method_arg_names_result_names_only(self) -> None: + class TestInterface( + DbusInterfaceCommonAsync, + interface_name="org.test.intro2", + ): + @dbus_method_async( + input_signature="ss", + result_signature="i", + result_args_names=("result",) + ) + async def login( + self, + user_name: str, + pin_code: str, + ) -> int: + return 0 + + obj, rem = initialize_object(TestInterface) + + introspection = await rem.dbus_introspect() + self.assertIn('name="user_name"', introspection) + self.assertIn('name="result"', introspection) + self.assertIn('name="pin_code"', introspection) + + async def test_method_arg_names_full(self) -> None: + class TestInterface( + DbusInterfaceCommonAsync, + interface_name="org.test.intro3", + ): + @dbus_method_async( + input_signature="ss", + input_args_names=("UserName", "PinCode"), + result_signature="i", + result_args_names=("Result",) + ) + async def login( + self, + user_name: str, + pin_code: str, + ) -> int: + return 0 + + obj, rem = initialize_object(TestInterface) + + introspection = await rem.dbus_introspect() + self.assertIn('name="UserName"', introspection) + self.assertIn('name="Result"', introspection) + self.assertIn('name="PinCode"', introspection) + + async def test_method_arg_names_no_return_args(self) -> None: + class TestInterface( + DbusInterfaceCommonAsync, + interface_name="org.test.intro4", + ): + @dbus_method_async( + input_signature="ss", + result_args_names=(), + ) + async def login( + self, + user_name: str, + pin_code: str, + ) -> None: + return None + + obj, rem = initialize_object(TestInterface) + + introspection = await rem.dbus_introspect() + self.assertIn('name="user_name"', introspection) + self.assertIn('name="pin_code"', introspection) diff --git a/test/test_sd_bus_sync.py b/test/test_sdbus_block.py similarity index 58% rename from test/test_sd_bus_sync.py rename to test/test_sdbus_block.py index 724e80e..ef650d3 100644 --- a/test/test_sd_bus_sync.py +++ b/test/test_sdbus_block.py @@ -17,15 +17,15 @@ # 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 __future__ import annotations from unittest import main +from sdbus.exceptions import DbusPropertyReadOnlyError from sdbus.unittest import IsolatedDbusTestCase from sdbus_block.dbus_daemon import FreedesktopDbus -from sdbus import DbusPropertyReadOnlyError +from sdbus import DbusInterfaceCommon, dbus_method class TestSync(IsolatedDbusTestCase): @@ -46,17 +46,19 @@ def test_sync(self) -> None: self.assertIsInstance( s.get_connection_uid('org.freedesktop.DBus'), int) - def test_invalid_assignment() -> None: + with self.assertRaises(DbusPropertyReadOnlyError): s.features = ['test'] - self.assertRaises(DbusPropertyReadOnlyError, test_invalid_assignment) - self.assertTrue(s.get_name_owner('org.example.test')) with self.subTest('Test dbus to python name map'): - self.assertEqual( - 'features', - s._dbus_to_python_name_map['Features'], + self.assertTrue( + any( + "Features" in meta.dbus_member_to_python_attr + for meta in ( + meta for _, meta in s._dbus_iter_interfaces_meta() + ) + ) ) with self.subTest('Test properties_get_all_dict'): @@ -73,6 +75,40 @@ def test_docstring(self) -> None: with self.subTest('Property doc (through class dict)'): self.assertTrue(getdoc(s.__class__.__dict__['features'])) + def test_interface_composition(self) -> None: + class OneInterface( + DbusInterfaceCommon, + interface_name="org.example.one", + ): + @dbus_method(result_signature="x") + def one(self) -> int: + raise NotImplementedError + + class TwoInterface( + DbusInterfaceCommon, + interface_name="org.example.two", + ): + @dbus_method(result_signature="t") + def two(self) -> int: + raise NotImplementedError + + class CombinedInterface(TwoInterface, OneInterface): + ... + + test_combined = CombinedInterface("org.test", "/") + test_combined_interfaces = [ + iface for iface, _ in test_combined._dbus_iter_interfaces_meta() + ] + + # Verify the order of reported interfaces on the combined class. + self.assertEqual(test_combined_interfaces, [ + "org.freedesktop.DBus.Peer", + "org.freedesktop.DBus.Introspectable", + "org.freedesktop.DBus.Properties", + "org.example.one", + "org.example.two", + ]) + if __name__ == '__main__': main() diff --git a/test/test_sdbus_block_bad_class.py b/test/test_sdbus_block_bad_class.py new file mode 100644 index 0000000..14855e7 --- /dev/null +++ b/test/test_sdbus_block_bad_class.py @@ -0,0 +1,203 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later + +# Copyright (C) 2023 igo95862 + +# This file is part of python-sdbus + +# 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 __future__ import annotations + +from gc import collect +from unittest import TestCase +from unittest import main as unittest_main + +from sdbus import DbusInterfaceCommon, dbus_method, dbus_property + +from .common_test_util import skip_if_no_name_validations + + +class GoodDbusInterface( + DbusInterfaceCommon, + interface_name="org.example.test", +): + @dbus_method() + def test_method(self) -> None: + raise NotImplementedError + + @dbus_property("s") + def test_property(self) -> str: + return "test" + + +class TestBadDbusClass(TestCase): + def test_method_name_override(self) -> None: + with self.subTest("Method override"), self.assertRaises(ValueError): + + class BadMethodOverrideClass(GoodDbusInterface): + def test_method(self) -> None: + return + + with self.subTest("D-Bus method override"), self.assertRaises( + ValueError + ): + + class BadDbusMethodOverrideClass(GoodDbusInterface): + @dbus_method() + def test_method(self) -> None: + return + + with self.subTest("Property override"), self.assertRaises(ValueError): + + class BadPropertyOverrideClass(GoodDbusInterface): + def test_property(self) -> str: # type: ignore + return "override" + + with self.subTest("D-Bus property override"), self.assertRaises( + ValueError + ): + + class BadDbusPropertyOverrideClass(GoodDbusInterface): + @dbus_property("s") + def test_property(self) -> str: + return "override" + + with self.subTest("Good new method"): + + class GoodSubclass(GoodDbusInterface): + def new_method(self) -> int: + return 1 + + def test_interface_collision(self) -> None: + with self.subTest("No collision"): + class NonInterface(GoodDbusInterface): + def do_work(self) -> None: + ... + + with self.subTest("Collision"), self.assertRaises(ValueError): + class NewExampleInterface( + DbusInterfaceCommon, + interface_name="org.example.test", + ): + ... + + def test_bad_class_names(self) -> None: + skip_if_no_name_validations() + + with self.assertRaisesRegex(AssertionError, "^Invalid interface name"): + + class BadInterfaceName( + DbusInterfaceCommon, + interface_name="0.test", + ): + ... + + with self.assertRaisesRegex( + AssertionError, + "^Invalid method name", + ): + + class BadMethodName( + DbusInterfaceCommon, + interface_name="org.example", + ): + @dbus_method( + result_signature="s", + method_name="🤫", + ) + def test(self) -> str: + return "test" + + with self.assertRaisesRegex( + AssertionError, + "^Invalid property name", + ): + + class BadPropertyName( + DbusInterfaceCommon, + interface_name="org.example", + ): + @dbus_property( + property_signature="s", + property_name="🤫", + ) + def test(self) -> str: + return "test" + + def test_dbus_elements_without_interface_name(self) -> None: + with self.assertRaisesRegex(TypeError, "without interface name"): + + class NoInterfaceName(DbusInterfaceCommon): + @dbus_method() + def example(self) -> None: + ... + + def test_shared_parent_class(self) -> None: + class One(GoodDbusInterface): + ... + + class Two(GoodDbusInterface): + ... + + class Shared(One, Two): + ... + + def test_combined_collision(self) -> None: + + class One( + DbusInterfaceCommon, + interface_name="org.example.foo", + ): + @dbus_method() + def example(self) -> None: + ... + + class Two( + DbusInterfaceCommon, + interface_name="org.example.bar", + ): + @dbus_method() + def example(self) -> None: + ... + + with self.assertRaisesRegex(ValueError, "collision"): + class Combined(One, Two): + ... + + def test_class_cleanup(self) -> None: + class One( + DbusInterfaceCommon, + interface_name="org.example.foo1", + ): + ... + + with self.assertRaises(ValueError): + class Two( + DbusInterfaceCommon, + interface_name="org.example.foo1", + ): + ... + + del One + collect() # Let weak refs be processed + + class After( + DbusInterfaceCommon, + interface_name="org.example.foo1", + ): + ... + + +if __name__ == "__main__": + unittest_main() diff --git a/test/test_sdbus_utils.py b/test/test_sdbus_utils.py new file mode 100644 index 0000000..49b443c --- /dev/null +++ b/test/test_sdbus_utils.py @@ -0,0 +1,281 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later + +# Copyright (C) 2024 igo95862 + +# This file is part of python-sdbus + +# 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 __future__ import annotations + +from unittest import TestCase + +from sdbus.unittest import IsolatedDbusTestCase +from sdbus.utils.inspect import inspect_dbus_bus, inspect_dbus_path +from sdbus.utils.parse import parse_get_managed_objects + +from sdbus import ( + DbusInterfaceCommon, + DbusInterfaceCommonAsync, + dbus_property, + dbus_property_async, + sd_bus_open_user, +) + +TEST_PATH = "/test" + + +class FooAsync(DbusInterfaceCommonAsync, interface_name="org.foo"): + @dbus_property_async("x") + def foo(self) -> int: + return 1 + + +class BarAsync(DbusInterfaceCommonAsync, interface_name="org.bar"): + @dbus_property_async("x") + def bar(self) -> int: + return 2 + + +class FooBarAsync(FooAsync, BarAsync): + ... + + +class Foo(DbusInterfaceCommon, interface_name="org.foo"): + @dbus_property("x") + def foo(self) -> int: + return 1 + + +class Bar(DbusInterfaceCommon, interface_name="org.bar"): + @dbus_property("x") + def bar(self) -> int: + return 2 + + +class FooBar(Foo, Bar): + ... + + +MANAGED_OBJECTS_COMBINED = { + "/test": { + "org.foo": {"Foo": ("x", 1)}, + "org.bar": {"Bar": ("x", 2)}, + } +} + +MANAGED_OBJECTS_SPLIT = { + "/foo": { + "org.foo": {"Foo": ("x", 1)}, + }, + "/bar": { + "org.bar": {"Bar": ("x", 2)}, + }, +} + +MANAGED_OBJECTS_BOTH = {**MANAGED_OBJECTS_COMBINED, **MANAGED_OBJECTS_SPLIT} + + +class TestSdbusUtilsParse(TestCase): + def test_parse_get_managed_objects_async_combined(self) -> None: + parsed_managed = parse_get_managed_objects( + FooBarAsync, + MANAGED_OBJECTS_COMBINED, + ) + + self.assertEqual(1, len(parsed_managed)) + + class_type, properties_data = parsed_managed["/test"] + self.assertEqual(FooBarAsync, class_type) + self.assertEqual(properties_data["foo"], 1) + self.assertEqual(properties_data["bar"], 2) + + def test_parse_get_managed_objects_block_combined(self) -> None: + parsed_managed = parse_get_managed_objects( + FooBar, + MANAGED_OBJECTS_COMBINED, + ) + + self.assertEqual(1, len(parsed_managed)) + + class_type, properties_data = parsed_managed["/test"] + self.assertEqual(FooBar, class_type) + self.assertEqual(properties_data["foo"], 1) + self.assertEqual(properties_data["bar"], 2) + + def test_parse_get_managed_objects_async_split(self) -> None: + parsed_managed = parse_get_managed_objects( + [FooAsync, BarAsync], + MANAGED_OBJECTS_SPLIT, + ) + + self.assertEqual(2, len(parsed_managed)) + + class_type, properties_data = parsed_managed["/foo"] + self.assertEqual(FooAsync, class_type) + self.assertEqual(properties_data["foo"], 1) + + class_type, properties_data = parsed_managed["/bar"] + self.assertEqual(BarAsync, class_type) + self.assertEqual(properties_data["bar"], 2) + + def test_parse_get_managed_objects_block_split(self) -> None: + parsed_managed = parse_get_managed_objects( + [Foo, Bar], + MANAGED_OBJECTS_SPLIT, + ) + + self.assertEqual(2, len(parsed_managed)) + + class_type, properties_data = parsed_managed["/foo"] + self.assertEqual(Foo, class_type) + self.assertEqual(properties_data["foo"], 1) + + class_type, properties_data = parsed_managed["/bar"] + self.assertEqual(Bar, class_type) + self.assertEqual(properties_data["bar"], 2) + + def test_parse_get_managed_objects_unknown_interface_error(self) -> None: + with self.assertRaisesRegex(KeyError, "org.foo"): + parse_get_managed_objects( + Bar, + MANAGED_OBJECTS_SPLIT, + ) + + def test_parse_get_managed_objects_unknown_interface_none_reuse( + self, + ) -> None: + parsed_managed = parse_get_managed_objects( + {BarAsync}, + MANAGED_OBJECTS_SPLIT, + on_unknown_interface="none", + on_unknown_member="reuse", + ) + + class_type, properties_data = parsed_managed["/foo"] + self.assertIsNone(class_type) + self.assertEqual(properties_data["Foo"], 1) + + class_type, properties_data = parsed_managed["/bar"] + self.assertEqual(BarAsync, class_type) + self.assertEqual(properties_data["bar"], 2) + + def test_parse_get_managed_objects_unknown_member_skip(self) -> None: + parsed_managed = parse_get_managed_objects( + [Foo], + MANAGED_OBJECTS_SPLIT, + on_unknown_interface="none", + on_unknown_member="ignore", + ) + + self.assertEqual(2, len(parsed_managed)) + + class_type, properties_data = parsed_managed["/foo"] + self.assertEqual(Foo, class_type) + self.assertEqual(properties_data["foo"], 1) + + class_type, properties_data = parsed_managed["/bar"] + self.assertIsNone(class_type) + self.assertEqual(0, len(properties_data)) + + def test_parse_get_managed_objects_interface_subset_single(self) -> None: + parsed_managed = parse_get_managed_objects( + [FooAsync], + MANAGED_OBJECTS_COMBINED, + on_unknown_interface="error", + on_unknown_member="reuse", + use_interface_subsets=True, + ) + class_type, properties_data = parsed_managed["/test"] + self.assertIs(class_type, FooAsync) + self.assertEqual(properties_data["foo"], 1) + self.assertEqual(properties_data["Bar"], 2) + + def test_parse_get_managed_objects_interface_subset_multiple(self) -> None: + parsed_managed = parse_get_managed_objects( + [FooAsync, FooBarAsync], + # FooBarAsync should be prioritized then both interfaces + # are available on the path. + MANAGED_OBJECTS_BOTH, + on_unknown_interface="none", + on_unknown_member="reuse", + use_interface_subsets=True, + ) + + class_type, _ = parsed_managed["/test"] + self.assertIs(class_type, FooBarAsync) + class_type, _ = parsed_managed["/foo"] + self.assertIs(class_type, FooAsync) + class_type, _ = parsed_managed["/bar"] + self.assertIsNone(class_type) + + +class TestSdbusUtilsInspect(IsolatedDbusTestCase): + def test_inspect_dbus_path_block(self) -> None: + proxy = DbusInterfaceCommon("example.org", TEST_PATH) + + self.assertEqual(inspect_dbus_path(proxy), TEST_PATH) + + new_bus = sd_bus_open_user() + + with self.assertRaisesRegex(LookupError, "is not attached to bus"): + inspect_dbus_path(proxy, new_bus) + + def test_inspect_dbus_path_async_proxy(self) -> None: + proxy = DbusInterfaceCommonAsync.new_proxy("example.org", TEST_PATH) + + self.assertEqual(inspect_dbus_path(proxy), TEST_PATH) + + new_bus = sd_bus_open_user() + + with self.assertRaisesRegex(LookupError, "is not attached to bus"): + inspect_dbus_path(proxy, new_bus) + + def test_inspect_dbus_path_async_local(self) -> None: + local_obj = FooBarAsync() + + with self.assertRaisesRegex( + LookupError, "is not exported to any D-Bus", + ): + inspect_dbus_path(local_obj) + + local_obj.export_to_dbus(TEST_PATH) + + self.assertEqual(inspect_dbus_path(local_obj), TEST_PATH) + + new_bus = sd_bus_open_user() + + with self.assertRaisesRegex(LookupError, "is not attached to bus"): + inspect_dbus_path(local_obj, new_bus) + + def test_inspect_attached_bus(self) -> None: + proxy = DbusInterfaceCommon("example.org", TEST_PATH) + + self.assertIs(inspect_dbus_bus(proxy), self.bus) + + with self.assertRaises(TypeError): + inspect_dbus_bus(object()) # type: ignore[arg-type] + + def test_inspect_attached_bus_async(self) -> None: + proxy = DbusInterfaceCommonAsync.new_proxy("example.org", TEST_PATH) + + self.assertIs(inspect_dbus_bus(proxy), self.bus) + + local_obj = FooBarAsync() + + self.assertIsNone(inspect_dbus_bus(local_obj)) + + local_obj.export_to_dbus("/") + + self.assertIs(inspect_dbus_bus(local_obj), self.bus) diff --git a/test/test_typing.py b/test/test_typing.py new file mode 100644 index 0000000..e6e956c --- /dev/null +++ b/test/test_typing.py @@ -0,0 +1,167 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later + +# Copyright (C) 2024 igo95862 + +# This file is part of python-sdbus + +# 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 __future__ import annotations + +from sdbus import ( + DbusInterfaceCommon, + DbusInterfaceCommonAsync, + dbus_method, + dbus_method_async, + dbus_property, + dbus_property_async, + dbus_signal_async, +) + + +class TestTypingBlocking( + DbusInterfaceCommon, + interface_name="example.com", +): + + @dbus_method(result_signature="as") + def get_str_list_method(self) -> list[str]: + raise NotImplementedError + + @dbus_property("as") + def str_list_property(self) -> list[str]: + raise NotImplementedError + + +# These functions are not meant to be executed +# but exist to be type checked. + +def check_blocking_interface_method_typing( + test_interface: TestTypingBlocking, +) -> None: + + should_be_list = test_interface.get_str_list_method() + should_be_list.append("test") + + for x in should_be_list: + x.capitalize() + + +def check_blocking_interface_property_typing( + test_interface: TestTypingBlocking, +) -> None: + + should_be_list = test_interface.str_list_property + should_be_list.append("test") + + for x in should_be_list: + x.capitalize() + + test_interface.str_list_property = ["test", "foobar"] + + +class TestTypingAsync( + DbusInterfaceCommonAsync, + interface_name="example.com", +): + + @dbus_method_async(result_signature="as") + async def get_str_list_method(self) -> list[str]: + raise NotImplementedError + + @dbus_property_async("as") + def str_list_property(self) -> list[str]: + raise NotImplementedError + + @dbus_signal_async("as") + def str_list_signal(self) -> list[str]: + raise NotImplementedError + + +async def check_async_interface_method_typing( + test_interface: TestTypingAsync, +) -> None: + + should_be_list = await test_interface.get_str_list_method() + should_be_list.append("test") + + for x in should_be_list: + x.capitalize() + + +async def check_async_interface_property_typing( + test_interface: TestTypingAsync, +) -> None: + + should_be_list = await test_interface.str_list_property + should_be_list.append("test") + + for x in should_be_list: + x.capitalize() + + should_be_list2 = await test_interface.str_list_property.get_async() + should_be_list2.append("test") + + for x in should_be_list2: + x.capitalize() + + await test_interface.str_list_property.set_async(["test", "foobar"]) + + +async def check_async_interface_signal_typing( + test_interface: TestTypingAsync, +) -> None: + + async for ls in test_interface.str_list_signal: + ls.append("test") + for x in ls: + x.capitalize() + + async for ls2 in test_interface.str_list_signal.catch(): + ls2.append("test") + for x2 in ls2: + x2.capitalize() + + async for p1, ls3 in test_interface.str_list_signal.catch_anywhere(): + p1.capitalize() + ls3.append("test") + for x3 in ls3: + x3.capitalize() + + async for p2, ls4 in ( + TestTypingAsync.str_list_signal + .catch_anywhere("org.example") + ): + p2.capitalize() + ls4.append("test") + for x4 in ls4: + x4.capitalize() + + test_interface.str_list_signal.emit(["test", "foobar"]) + + +async def check_async_element_class_access_typing() -> None: + + test_list: list[str] = [] + + # TODO: Fix dbus async method typing + # test_list.append( + # TestTypingAsync.get_str_list_method.method_name + # ) + test_list.append( + TestTypingAsync.str_list_property.property_name + ) + test_list.append( + TestTypingAsync.str_list_signal.signal_name + ) diff --git a/tools/run_py_linters.py b/tools/run_py_linters.py index a9e858e..091ae00 100755 --- a/tools/run_py_linters.py +++ b/tools/run_py_linters.py @@ -22,8 +22,7 @@ from argparse import ArgumentParser from os import environ from pathlib import Path -from subprocess import run -from typing import List +from subprocess import SubprocessError, run source_root = Path(environ['MESON_SOURCE_ROOT']) build_dir = Path(environ['MESON_BUILD_ROOT']) @@ -49,9 +48,9 @@ def run_mypy() -> None: print('Running mypy on all modules') run( args=( - 'mypy', '--strict', + 'mypy', '--strict', '--pretty', '--cache-dir', mypy_cache_dir, - '--python-version', '3.8', + '--python-version', '3.9', '--namespace-packages', '--explicit-package-bases', *all_python_modules, @@ -61,7 +60,7 @@ def run_mypy() -> None: ) -def linter_main() -> None: +def run_flake8() -> None: run( args=( 'flake8', @@ -70,11 +69,26 @@ def linter_main() -> None: check=True, ) - run_mypy() +def linter_main() -> None: + is_success = True + + try: + run_flake8() + except SubprocessError: + is_success = False + + try: + run_mypy() + except SubprocessError: + is_success = False -def get_all_python_files() -> List[Path]: - python_files: List[Path] = [source_root / 'setup.py'] + if not is_success: + raise SystemExit(1) + + +def get_all_python_files() -> list[Path]: + python_files: list[Path] = [source_root / 'setup.py'] for python_module in all_python_modules: if python_module.is_dir(): @@ -88,10 +102,9 @@ def get_all_python_files() -> List[Path]: def formater_main() -> None: - all_python_files = get_all_python_files() run( - args=('autopep8', '--in-place', *all_python_files), + args=('autopep8', '--recursive', '--in-place', *all_python_modules), check=True, ) @@ -100,7 +113,7 @@ def formater_main() -> None: 'isort', '-m', 'VERTICAL_HANGING_INDENT', '--trailing-comma', - *all_python_files, + *all_python_modules, ), check=True, ) diff --git a/tox.ini b/tox.ini deleted file mode 100644 index 21846b4..0000000 --- a/tox.ini +++ /dev/null @@ -1,6 +0,0 @@ -[tox] -envlist = py38,py39 - -[testenv] -commands = python -m unittest --verbose - diff --git a/wheel-build/audit_wheel_wrapper.py b/wheel-build/audit_wheel_wrapper.py new file mode 100644 index 0000000..ec29ee7 --- /dev/null +++ b/wheel-build/audit_wheel_wrapper.py @@ -0,0 +1,27 @@ +# SPDX-License-Identifier: MPL-2.0 +# SPDX-FileCopyrightText: 2024 igo95862 +from __future__ import annotations + +from argparse import ArgumentParser +from unittest.mock import patch + +from auditwheel.main import main as auditwheel_main # type: ignore + + +def main(arch: str, wrapped_args: list[str]) -> None: + with patch("sys.argv", [""] + wrapped_args), patch( + "platform.machine", return_value=arch + ): + auditwheel_main() + + +if __name__ == "__main__": + arg_parse = ArgumentParser() + arg_parse.add_argument( + "--arch", + choices=("x86_64", "i686", "aarch64", "armv7l"), + default="x86_64", + ) + arg_parse.add_argument("wrapped_args", nargs="*") + + main(**vars(arg_parse.parse_args())) diff --git a/wheel-build/build_container_archive.py b/wheel-build/build_container_archive.py deleted file mode 100755 index 9a3ebcf..0000000 --- a/wheel-build/build_container_archive.py +++ /dev/null @@ -1,150 +0,0 @@ -#!/usr/bin/python3 -# SPDX-License-Identifier: LGPL-2.1-or-later - -# Copyright (C) 2020, 2021 igo95862 - -# This file is part of python-sdbus - -# 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 __future__ import annotations - -from argparse import ArgumentParser -from pathlib import Path -from shutil import copy -from subprocess import PIPE, run -from tempfile import TemporaryDirectory - -SYSTEMD_VERSION = '249.12' -UTIL_LINUX_VERSION = '2.37' -NINJA_VERSION = '1.10.2' -LIBCAP_VERSION = '2.64' - - -def create_archive(build_root: Path, output_file: Path) -> None: - run( - ['tar', '--create', '--file', str(output_file.absolute()), '.'], - cwd=build_root.resolve(), - check=True, - ) - - -def download_and_unpack_source(target_dir: Path, url: str) -> None: - target_dir.mkdir(exist_ok=True) # TODO: maybe delete folder - - with TemporaryDirectory() as tmpdir: - tmpdir_path = Path(tmpdir) - dowload_tar_path = tmpdir_path / 'donwload.tar.gz' - - run( - ['curl', '--fail', '--location', - url, '--output', str(dowload_tar_path)], - check=True, - ) - - run( - ['tar', - '--directory', str(target_dir), - '--strip-components=1', - '--extract', '--file', str(dowload_tar_path)], - check=True, - ) - - -def download_systemd_source(build_dir: Path) -> None: - systemd_download_url = ( - "https://github.com/systemd/systemd-stable/" - f"archive/refs/tags/v{SYSTEMD_VERSION}.tar.gz" - ) - systemd_src_dir = build_dir / "src_systemd" - systemd_src_dir.mkdir(exist_ok=True) - - util_linux_url = ( - "https://mirrors.edge.kernel.org/pub/linux/utils/util-linux/" - f"v{UTIL_LINUX_VERSION}/util-linux-{UTIL_LINUX_VERSION}.tar.xz" - ) - util_linux_src_dir = build_dir / "src_util_linux" - - ninja_src_url = ( - "https://github.com/ninja-build/ninja/" - f"archive/refs/tags/v{NINJA_VERSION}.tar.gz" - ) - ninja_src_dir = build_dir / "src_ninja" - - libcap_src_url = ( - "https://kernel.org/pub/linux/libs/security/" - f"linux-privs/libcap2/libcap-{LIBCAP_VERSION}.tar.xz" - ) - libcap_src_dir = build_dir / 'src_libcap' - - download_and_unpack_source(systemd_src_dir, systemd_download_url) - download_and_unpack_source(util_linux_src_dir, util_linux_url) - download_and_unpack_source(ninja_src_dir, ninja_src_url) - download_and_unpack_source(libcap_src_dir, libcap_src_url) - - -def copy_git_ls_files(source_root: Path, build_root: Path) -> None: - git_ls = run( - ['git', 'ls-files'], - stdout=PIPE, - cwd=source_root.resolve(), - text=True, - check=True, - ) - - for file_relative_source_str in git_ls.stdout.splitlines(): - orig_file_path = source_root / file_relative_source_str - copy_file_path = build_root / "python-sdbus" / file_relative_source_str - if not orig_file_path.exists(): - raise ValueError('Path does not exist', orig_file_path) - - if orig_file_path.is_dir(): - continue - else: - copy_file_path.parent.mkdir(parents=True, exist_ok=True) - copy(orig_file_path, copy_file_path.parent) - - -def main() -> None: - parser = ArgumentParser() - parser.add_argument( - '--build-dir', - type=Path, - required=True, - ) - parser.add_argument( - '--output-file', - type=Path, - required=True, - ) - parser.add_argument( - '--source-root', - type=Path, - required=True, - ) - args = parser.parse_args() - - build_dir = args.build_dir - output_file = args.output_file - source_root = args.source_root - - copy_git_ls_files(source_root, build_dir) - download_systemd_source(build_dir) - - create_archive(build_dir, output_file) - - -if __name__ == '__main__': - main() diff --git a/wheel-build/consistent_interface_order.patch b/wheel-build/consistent_interface_order.patch new file mode 100644 index 0000000..e101fa1 --- /dev/null +++ b/wheel-build/consistent_interface_order.patch @@ -0,0 +1,30 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# SPDX-FileCopyrightText: 2025 Arkadiusz Bokowy +From 998aa62a21c69b34700f6cbbeb540beddffa9c89 Mon Sep 17 00:00:00 2001 +From: Arkadiusz Bokowy +Date: Thu, 12 Jun 2025 16:20:29 +0200 +Subject: [PATCH] sd-bus: Preserve interfaces addition order + +When adding a new interface to the object add it at the end of the list. +This way, when iterating over the list, e.g., during handling introspect +call, the order of returned interfaces will mach the order in which they +were added. +--- + src/libsystemd/sd-bus/bus-objects.c | 3 +++ + test/units/TEST-23-UNIT-FILE.oneshot-restart.sh | 2 +- + 2 files changed, 4 insertions(+), 1 deletion(-) + +diff --git a/src/libsystemd/sd-bus/bus-objects.c b/src/libsystemd/sd-bus/bus-objects.c +index 7309ad621a0a7..cc1ef226f0cdd 100644 +--- a/src/libsystemd/sd-bus/bus-objects.c ++++ b/src/libsystemd/sd-bus/bus-objects.c +@@ -1973,6 +1973,9 @@ static int add_object_vtable_internal( + } + } + ++ if (!existing) ++ existing = LIST_FIND_TAIL(vtables, n->vtables); ++ + s->node_vtable.node = n; + LIST_INSERT_AFTER(vtables, n->vtables, existing, &s->node_vtable); + bus->nodes_modified = true; diff --git a/wheel-build/meson.build b/wheel-build/meson.build deleted file mode 100644 index 07bc568..0000000 --- a/wheel-build/meson.build +++ /dev/null @@ -1,36 +0,0 @@ -archive_builder = find_program('./build_container_archive.py') -podman_runner = find_program('./run_podman.py') -container_script = files('run_inside_container.py') - -build_container_archive = custom_target( - 'container_archive.tar', - build_by_default : false, - output : 'container_archive.tar', - input : container_script, # Force archive rebuilt on script changes - command : [ - archive_builder, - '--build-dir', '@PRIVATE_DIR@', - '--output-file', '@OUTPUT@', - '--source-root', '@SOURCE_ROOT@', - ], -) - -run_podman_x86_64 = run_target( - 'run_podman_x86_64', - command : [ - podman_runner, - '--archive', build_container_archive, - '--source-root', '@SOURCE_ROOT@', - '--arch', 'x86_64', - ], -) - -run_podman_aarch64 = run_target( - 'run_podman_aarch64', - command : [ - podman_runner, - '--archive', build_container_archive, - '--source-root', '@SOURCE_ROOT@', - '--arch', 'aarch64', - ], -) diff --git a/wheel-build/run_inside_container.py b/wheel-build/run_inside_container.py deleted file mode 100755 index b69e860..0000000 --- a/wheel-build/run_inside_container.py +++ /dev/null @@ -1,246 +0,0 @@ -#!/opt/python/cp39-cp39/bin/python3 -# SPDX-License-Identifier: LGPL-2.1-or-later - -# Copyright (C) 2020, 2021 igo95862 - -# This file is part of python-sdbus - -# 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 __future__ import annotations - -from os import environ, execl -from pathlib import Path -from shutil import copy -from subprocess import PIPE, CalledProcessError, run -from typing import List - -yum_packages: List[str] = [ - 'gettext-autopoint', 'gperf', -] - -# env -# export PATH="/opt/python/cp39-cp39/bin:${PATH}" - -# util-linux -# AL_OPTS="-I/usr/share/aclocal/" ./autogen.sh -# ./configure -# --prefix '/usr/local' --libdir '/usr/local/lib64' -# --enable-symvers -# --with-pkgconfigdir '/usr/share/pkgconfig/' - -# Ninja -# ./configure.py --boostrap -# cp ./ninja /usr/local/bin - -# systemd -# export PKG_CONFIG_PATH="/usr/local/lib64/pkgconfig" -# meson setup build -Dstatic-libsystemd=pic - -# PYTHON_SDBUS_USE_STATIC_LINK=1 - -ROOT_DIR = Path('/root') -NPROC = '4' -PYTHON_VERSIONS = ['cp39-cp39', 'cp38-cp38', 'cp37-cp37m'] - -BASIC_C_FLAGS: List[str] = [ - '-O2', '-fno-plt', '-D_FORTIFY_SOURCE=2', - '-fstack-clash-protection', -] - - -def setup_env() -> None: - python_bin_paths = (f"/opt/python/{x}/bin" for x in PYTHON_VERSIONS) - - environ['PATH'] = f"{':'.join(python_bin_paths)}:{environ['PATH']}" - environ['PYTHON_SDBUS_USE_STATIC_LINK'] = '1' - - audit_wheel_arch = environ['AUDITWHEEL_ARCH'] - - if audit_wheel_arch == 'x86_64': - BASIC_C_FLAGS.extend( - ( - '-march=x86-64', '-mtune=generic', - '-fcf-protection', # cf-protection only available on x86_64 - ) - ) - elif audit_wheel_arch == 'aarch64': - BASIC_C_FLAGS.extend(('-march=armv8-a', '-mtune=generic')) - else: - print('PYTHON-SDBUS: Unknown arch') - - new_cflags = ' '.join(BASIC_C_FLAGS) - environ['CFLAGS'] = new_cflags - environ['CXXFLAGS'] = new_cflags - - nproc = run( - ['nproc'], - stdout=PIPE, - text=True, - check=True, - ) - - global NPROC - NPROC = nproc.stdout.splitlines()[0] - - -def install_packages() -> None: - run( - ['yum', 'install', '--assumeyes'] + yum_packages, - check=True, - ) - - -def install_ninja() -> None: - ninja_src_path = ROOT_DIR / 'src_ninja' - ninja_boot_strap_path = ninja_src_path / 'configure.py' - - run( - [ninja_boot_strap_path, '--bootstrap'], - cwd=ninja_src_path, - check=True, - ) - - copy(ninja_src_path / 'ninja', '/usr/local/bin') - - -def install_meson() -> None: - run( - ['pip3', 'install', 'meson==0.62', 'Jinja2==3.1.1'], - check=True, - ) - - -def install_util_linux() -> None: - util_linux_src_path = ROOT_DIR / 'src_util_linux' - - run( - [util_linux_src_path / 'autogen.sh'], - cwd=util_linux_src_path, - env={'AL_OPTS': '-I/usr/share/aclocal/', **environ}, - check=True, - ) - - run( - [ - util_linux_src_path / 'configure', - '--prefix', '/usr/local', - '--libdir', '/usr/local/lib64', - '--enable-symvers', - ], - cwd=util_linux_src_path, - check=True, - ) - - run( - ['make', '--jobs', NPROC, 'install'], - cwd=util_linux_src_path, - check=True, - ) - - -def install_libcap() -> None: - libcap_src_path = ROOT_DIR / 'src_libcap' - - run( - ['make', '--jobs', NPROC, 'install'], - cwd=libcap_src_path, - check=True, - ) - - -def install_systemd() -> None: - systemd_src_path = ROOT_DIR / 'src_systemd' - systemd_build_path = ROOT_DIR / 'build_systemd' - - run( - ['meson', 'setup', - systemd_build_path, systemd_src_path, - '-Dstatic-libsystemd=pic', - '--buildtype', 'plain', - '-Db_lto=true', '-Db_pie=true', - ], - env={**environ, 'PKG_CONFIG_PATH': '/usr/local/lib64/pkgconfig'}, - check=True, - ) - - run( - ['ninja', 'install'], - cwd=systemd_build_path, - check=True, - ) - - -def compile_extension() -> None: - python_sdbus_src_path = ROOT_DIR / 'python-sdbus' - setup_py_path = python_sdbus_src_path / 'setup.py' - build_dir_path = python_sdbus_src_path / 'build' - dist_dir_path = python_sdbus_src_path / 'dist' - repaired_wheels_path = ROOT_DIR / 'wheels' - - run( - [ - 'python3.8', setup_py_path, - 'build', 'bdist_wheel', - '--py-limited-api', 'cp37', - ], - cwd=python_sdbus_src_path, - check=True, - env={**environ, 'PYTHON_SDBUS_USE_LIMITED_API': '1'}, - ) - - run( - ['rm', '--recursive', build_dir_path], - cwd=python_sdbus_src_path, - check=True, - ) - - # Repair wheels - for wheel in dist_dir_path.iterdir(): - run( - [ - 'auditwheel', 'repair', - '--plat', environ['AUDITWHEEL_PLAT'], - '--strip', - '--wheel-dir', repaired_wheels_path, - wheel, - ], - check=True, - ) - - -def drop_to_shell() -> None: - execl('/bin/sh', '/bin/sh') - - -def main() -> None: - setup_env() - install_packages() - - install_ninja() - install_meson() - - install_util_linux() - install_libcap() - install_systemd() - - compile_extension() - - -if __name__ == '__main__': - try: - main() - except CalledProcessError: - drop_to_shell() diff --git a/wheel-build/run_podman.py b/wheel-build/run_podman.py deleted file mode 100755 index c4e8285..0000000 --- a/wheel-build/run_podman.py +++ /dev/null @@ -1,91 +0,0 @@ -#!/usr/bin/python3 -# SPDX-License-Identifier: LGPL-2.1-or-later - -# Copyright (C) 2020, 2021 igo95862 - -# This file is part of python-sdbus - -# 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 __future__ import annotations - -from argparse import ArgumentParser -from pathlib import Path -from shutil import copy -from subprocess import run -from tempfile import TemporaryDirectory - -MANYLINUX_VERSION = 'manylinux2014' - - -def run_podman( - archive: Path, - source_root: Path, - arch: str,) -> None: - wheels_root = source_root / 'dist' - - with TemporaryDirectory() as tmpdir: - run( - ['tar', '--extract', - '--directory', tmpdir, - '--file', str(archive)], - check=True, - ) - run( - ['podman', 'run', - '--arch', arch, - '--tty', '--interactive', '--rm', - '--volume', '.:/root', - f"quay.io/pypa/{MANYLINUX_VERSION}_{arch}", - '/root/python-sdbus/wheel-build/run_inside_container.py', - ], - cwd=tmpdir, - check=True, - ) - - wheels_root.mkdir(exist_ok=True) - for wheel in (Path(tmpdir) / 'wheels').iterdir(): - copy(wheel, wheels_root) - - -def main() -> None: - parser = ArgumentParser() - parser.add_argument( - '--archive', - type=Path, - required=True, - ) - parser.add_argument( - '--source-root', - type=Path, - required=True, - ) - parser.add_argument( - '--arch', - type=str, - choices=['x86_64', 'aarch64'], - default='x86_64', - ) - - args = parser.parse_args() - run_podman( - args.archive, - args.source_root, - args.arch, - ) - - -if __name__ == '__main__': - main() diff --git a/wheel-build/run_podman_full_build.py b/wheel-build/run_podman_full_build.py new file mode 100644 index 0000000..991706e --- /dev/null +++ b/wheel-build/run_podman_full_build.py @@ -0,0 +1,349 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later + +# Copyright (C) 2025 igo95862 + +# This file is part of python-sdbus + +# 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 __future__ import annotations + +from argparse import ArgumentParser +from collections.abc import Callable, Iterator +from functools import partial +from pathlib import Path +from subprocess import PIPE +from subprocess import run as _run + +SDBUS_REFSPEC = "HEAD" +SDBUS_SRC_DIR = Path("/root/sdbus") + +WHEEL_BUILD_DIR = Path(__file__).parent +PROJECT_ROOT = WHEEL_BUILD_DIR.parent +BUILD_DIR = PROJECT_ROOT / "build/wheel-build/" +LAST_STAGE_FILE = BUILD_DIR / "last_stage" + +CONTAINER_IMAGE = "docker.io/debian:11-slim" +CONTAINER_NAME = "python-sdbus-build" +CONTAINER_ARCH = "x86_64" +DEBIAN_PACKAGES = ( + "python3", + "python3-dev", + "gcc", "gperf", + "meson", + "python3-wheel", + "python-setuptools", + "python3-jinja2", + "libcap-dev", + "libmount-dev", + "git", + "ca-certificates", + "pkg-config", +) +DEBIAN_NAME = "bullseye" + +BASIC_CFLAGS: list[str] = [ + '-O2', '-fno-plt', '-D_FORTIFY_SOURCE=2', + '-fstack-clash-protection', +] + +SYSTEMD_REPO = "https://github.com/systemd/systemd-stable.git" +# systemd 255 is last one before glibc 2.31 requirement +SYSTEMD_TAG = "v255.22" +SYSTEMD_SRC_DIR = Path("/root/systemd") +SYSTEMD_BUILD_DIR = SYSTEMD_SRC_DIR / "build" +SYSTEMD_COMPAT_PATCHES: list[str] = [ + "systemd_no_gettid_no_getdents64.patch", + "consistent_interface_order.patch", +] +SYSTEMD_OPTIONS: list[str] = [ + "static-libsystemd=pic", + "tests=false", + "coredump=false", + "dbus=false", + "efi=false", + "elfutils=false", + "hostnamed=false", + "homed=false", + "importd=false", + "initrd=false", + "kernel-install=false", + "logind=false", + "machined=false", + "man=false", + "networkd=false", + "portabled=false", + "repart=false", + "sysext=false", + "sysusers=false", + "timedated=false", + "timesyncd=false", + "tmpfiles=false", + "oomd=false", + "hibernate=false", + "nss-systemd=false", + "nss-resolve=false", +] + +run = partial(_run, check=True, cwd=PROJECT_ROOT) + + +def podman_exec( + *args: str, + env: dict[str, str] | None = None, + cwd: Path | None = None, + input: bytes | None = None, +) -> None: + + env_list = [ + f"--env={env_k}={env_v}" for env_k, env_v in env.items() + ] if env else [] + workdir_options = [f"--workdir={cwd}"] if cwd else [] + + run( + args=( + "podman", + "exec", + *env_list, + *workdir_options, + "--tty" if input is None else "--interactive", + CONTAINER_NAME, + *args, + ), + input=input, + ) + + +def podman_cp(src: Path, dest: Path, to_contatiner: bool = True) -> None: + if to_contatiner: + src_str = str(src.absolute()) + dest_str = f"{CONTAINER_NAME}:{dest}" + else: + src_str = f"{CONTAINER_NAME}:{src}" + dest_str = str(dest.absolute()) + + run( + args=("podman", "cp", src_str, dest_str) + ) + + +def podman_start() -> None: + run( + args=( + "podman", + "run", + "--name", CONTAINER_NAME, + "--arch", CONTAINER_ARCH, + "--detach", + "--rm", "--init", + CONTAINER_IMAGE, + "sleep", "3d", + ) + ) + + +def install_packages() -> None: + target_release = ("--target-release", f"{DEBIAN_NAME}-backports") + deb_env = {"DEBIAN_FRONTEND": "noninteractive"} + podman_exec( + "bash", + "-c", + "echo 'deb http://archive.debian.org/debian " + f"{DEBIAN_NAME}-backports main'" + " > /etc/apt/sources.list.d/backports.list" + ) + podman_exec("apt-get", "update", env=deb_env) + podman_exec( + "apt-get", + "upgrade", + *target_release, + "--yes", + env=deb_env, + ) + podman_exec( + "apt-get", + "install", + *target_release, + "--yes", + "--no-install-recommends", + *DEBIAN_PACKAGES, + env=deb_env, + ) + + +def clone_systemd() -> None: + podman_exec( + "git", "clone", + "--depth", "1", + "--branch", SYSTEMD_TAG, + "--", + SYSTEMD_REPO, + str(SYSTEMD_SRC_DIR), + ) + + +def apply_systemd_patch() -> None: + for patch_filename in SYSTEMD_COMPAT_PATCHES: + podman_cp(WHEEL_BUILD_DIR / patch_filename, SYSTEMD_SRC_DIR) + podman_exec( + "git", "apply", patch_filename, + cwd=SYSTEMD_SRC_DIR, + ) + + +def build_systemd() -> None: + systemd_options_get = (f"-D{o}" for o in SYSTEMD_OPTIONS) + cflags = {"CFLAGS": " ".join(BASIC_CFLAGS)} + podman_exec( + "meson", + "setup", + "--auto-features=disabled", + "--buildtype=release", + *systemd_options_get, + str(SYSTEMD_BUILD_DIR), + cwd=SYSTEMD_SRC_DIR, + env=cflags, + ) + podman_exec( + "meson", + "compile", + "systemd:static_library", + "libsystemd.pc", + cwd=SYSTEMD_BUILD_DIR, + ) + + +def install_systemd_files() -> None: + podman_exec( + "bash", + "-c", + "cp libsystemd.a" + " /usr/lib/$(cat /usr/lib/pkg-config.multiarch)/", + cwd=SYSTEMD_BUILD_DIR, + ) + podman_exec( + "cp", + "src/libsystemd/libsystemd.pc", + "/usr/share/pkgconfig/", + cwd=SYSTEMD_BUILD_DIR, + ) + podman_exec( + "cp", + "src/libsystemd/libsystemd.pc", + "/usr/share/pkgconfig/", + cwd=SYSTEMD_BUILD_DIR, + ) + podman_exec( + "mkdir", "--parents", "/usr/include/systemd/" + ) + required_headers = ( + "_sd-common.h", + "sd-id128.h", + "sd-daemon.h", + "sd-bus.h", + "sd-bus-vtable.h", + "sd-bus-protocol.h", + "sd-device.h", + "sd-event.h", + ) + podman_exec( + "cp", + *(f"src/systemd/{h}" for h in required_headers), + "/usr/include/systemd/", + cwd=SYSTEMD_SRC_DIR, + ) + + +def copy_sdbus_sources() -> None: + podman_exec("mkdir", "--parents", str(SDBUS_SRC_DIR)) + sdbus_tar = run( + args=("git", "archive", SDBUS_REFSPEC), + stdout=PIPE, + ).stdout + assert isinstance(sdbus_tar, bytes) + print("python-sdbus source archive size:", len(sdbus_tar)) + podman_exec( + "tar", "--extract", "--verbose", + cwd=SDBUS_SRC_DIR, + input=sdbus_tar, + ) + + +def compile_sdbus() -> None: + podman_exec( + "python3", "setup.py", "build", "bdist_wheel", + "--py-limited-api", "cp39", + cwd=SDBUS_SRC_DIR, + env={ + "PYTHON_SDBUS_USE_STATIC_LINK": "1", + "PYTHON_SDBUS_USE_LIMITED_API": "1", + "CFLAGS": " ".join(BASIC_CFLAGS), + }, + ) + + +def copy_dist() -> None: + podman_cp( + SDBUS_SRC_DIR / "dist", + BUILD_DIR / f"{CONTAINER_ARCH}-dist", + to_contatiner=False, + ) + + +STAGES: dict[str, Callable[[], None]] = { + "podman_start": podman_start, + "install_packages": install_packages, + "clone_systemd": clone_systemd, + "apply_systemd_patch": apply_systemd_patch, + "build_systemd": build_systemd, + "install_systemd_files": install_systemd_files, + "copy_sdbus_sources": copy_sdbus_sources, + "compile_sdbus": compile_sdbus, + "copy_dist": copy_dist, +} + + +def iter_stages() -> Iterator[tuple[str, Callable[[], None]]]: + stages_iter = iter(STAGES.items()) + if LAST_STAGE_FILE.exists(): + last_stage = LAST_STAGE_FILE.read_text().strip() + for stage_name, _ in stages_iter: + if stage_name == last_stage: + print("Last completed stage:", stage_name) + break + else: + print("Skipping stage:", stage_name) + + yield from stages_iter + + +def main() -> None: + args_parser = ArgumentParser() + args_parser.add_argument("--arch") + args = args_parser.parse_args() + + if arch := args.arch: + global CONTAINER_ARCH + CONTAINER_ARCH = arch + + BUILD_DIR.mkdir(parents=True, exist_ok=True) + + for stage_name, stage_func in iter_stages(): + stage_func() + LAST_STAGE_FILE.write_text(stage_name) + print("Completed:", stage_name) + + +if __name__ == "__main__": + main() diff --git a/wheel-build/systemd_no_gettid_no_getdents64.patch b/wheel-build/systemd_no_gettid_no_getdents64.patch new file mode 100644 index 0000000..138e2d9 --- /dev/null +++ b/wheel-build/systemd_no_gettid_no_getdents64.patch @@ -0,0 +1,30 @@ +diff --git a/meson.build b/meson.build +index 8c16c1c5c0..f34e4a0c3a 100644 +--- a/meson.build ++++ b/meson.build +@@ -573,8 +573,6 @@ endforeach + + foreach ident : [ + ['memfd_create', '''#include '''], +- ['gettid', '''#include +- #include '''], + ['fchmodat2', '''#include + #include '''], # no known header declares fchmodat2 + ['pivot_root', '''#include +@@ -631,13 +629,15 @@ foreach ident : [ + ['fsopen', '''#include '''], + ['fsconfig', '''#include '''], + ['fsmount', '''#include '''], +- ['getdents64', '''#include '''], + ] + + have = cc.has_function(ident[0], prefix : ident[1], args : '-D_GNU_SOURCE') + conf.set10('HAVE_' + ident[0].to_upper(), have) + endforeach + ++conf.set10('HAVE_GETTID', false) ++conf.set10('HAVE_GETDENTS64', false) ++ + if cc.has_function('getrandom', prefix : '''#include ''', args : '-D_GNU_SOURCE') + conf.set10('USE_SYS_RANDOM_H', true) + conf.set10('HAVE_GETRANDOM', true)