diff --git a/docs/source/high-level-service/index.rst b/docs/source/high-level-service/index.rst index ac4755aa..caad9671 100644 --- a/docs/source/high-level-service/index.rst +++ b/docs/source/high-level-service/index.rst @@ -24,7 +24,8 @@ on the class with the decorator methods :func:`@dbus_method() `, :func:`@dbus_property() `, and :func:`@dbus_signal() `. The parameters of the decorated class -methods must be annotated with DBus type strings to indicate the types +methods must be annotated using :class:`typing.Annotated` with a +:class:`DBusSignature ` to indicate the types of values they expect. See the documentation on `the type system `_ for more information on how DBus types are mapped to Python values with signature strings. The decorator methods @@ -56,9 +57,10 @@ lost. A class method decorated with ``@dbus_signal()`` will be exposed as a DBus signal. The value returned by the class method will be emitted as a signal and broadcast to clients who are listening to the signal. The -returned value must conform to the return annotation of the class method -as a DBus signature string. If the signal has more than one argument, -they must be returned within a ``tuple``. +returned value must conform to the return annotation of the class method. +The annotation must be as a :class:`typing.Annotated` with a +:class:`DBusSignature `. +If the signal has more than one argument, they must be returned within a ``tuple``. A class method decorated with ``@dbus_method()`` or ``@dbus_property()`` may throw a :class:`DBusError ` to return a diff --git a/src/dbus_fast/_private/util.py b/src/dbus_fast/_private/util.py index bec5dc59..b24dd75b 100644 --- a/src/dbus_fast/_private/util.py +++ b/src/dbus_fast/_private/util.py @@ -5,6 +5,7 @@ import re from collections.abc import Callable from typing import Annotated, Any, get_args, get_origin +from warnings import warn from dbus_fast.annotations import DBusSignature @@ -120,10 +121,18 @@ def parse_annotation(annotation: Any, module: Any) -> str: # a way to distinguish between a string constant and a forward reference # other than by heuristics. + # TODO: Change this to FutureWarning in 2027 and remove support for + # string annotations in 2028. + # If it looks like a dbus signature, return it directly. These are sorted # in the order of the "Summary of types" table in the D-Bus spec to make # verification easier. if re.match(r"^[ybnqiuxtdsoga\(\)v\{\}h]+$", annotation): + warn( + "String annotations are deprecated and support will be removed in the future. Use typing.Annotated with the appropriate annotation from dbus_fast.annotations instead.", + DeprecationWarning, + stacklevel=2, + ) return annotation # Otherwise, assume deferred evaluation of annotations. @@ -132,7 +141,13 @@ def parse_annotation(annotation: Any, module: Any) -> str: # It could be a string literal, e.g "'s'", in which case this will # effectively strip the quotes. Other literals would pass here, but # they aren't expected, so we just let those fail later. - return ast.literal_eval(annotation) + literal = ast.literal_eval(annotation) + warn( + "String annotations are deprecated and support will be removed in the future. Use typing.Annotated with the appropriate annotation from dbus_fast.annotations instead.", + DeprecationWarning, + stacklevel=2, + ) + return literal except ValueError: # Anything that isn't a Python literal will raise ValueError. pass diff --git a/src/dbus_fast/service.py b/src/dbus_fast/service.py index 6e81c1c5..120fa830 100644 --- a/src/dbus_fast/service.py +++ b/src/dbus_fast/service.py @@ -108,33 +108,36 @@ def dbus_method( :param disabled: If set to true, the method will not be visible to clients. :type disabled: bool - :example: - - :: + :class:`typing.Annotated` is used to specify the D-Bus signature along with + the Python type:: @dbus_method() - def echo(self, val: 's') -> 's': + def echo(self, val: DBusStr) -> DBusStr: return val @dbus_method() - def echo_two(self, val1: 's', val2: 'u') -> 'su': + def echo_two( + self, val1: DBusStr, val2: DBusUInt32 + ) -> Annotated[tuple[str, int], DBusSignature("su")]: return val1, val2 - If you use Python annotations for type hints, you can use :class:`typing.Annotated` - to specify the Python type and the D-Bus signature at the same time like this:: - - from dbus_fast.annotations import DBusSignature, DBusStr, DBusUInt32 + Originally, D-Bus signature strings were used directly in the annotations:: @dbus_method() - def echo(self, val: DBusStr) -> DBusStr: + def echo(self, val: 's') -> 's': return val @dbus_method() - def echo_two( - self, val1: DBusStr, val2: DBusUInt32 - ) -> Annotated[tuple[str, int], DBusSignature("su")]: + def echo_two(self, val1: 's', val2: 'u') -> 'su': return val1, val2 + Such usage is now deprecated and support will be removed in the future. + + .. versionchanged:: v4.1.0 + String annotations are deprecated and will raise a warning. Use + :class:`typing.Annotated` with the appropriate annotation from + :mod:`dbus_fast.annotations` instead. + .. versionchanged:: v4.0.0 :class:`typing.Annotated` can now be used to provide type hints and the D-Bus signature at the same time. Older versions require D-Bus signature @@ -375,31 +378,34 @@ def dbus_property( clients. :type disabled: bool - :example: - - :: + :class:`typing.Annotated` is used to specify the Python type and the D-Bus + signature at the same time like this:: @dbus_property() - def string_prop(self) -> 's': + def string_prop(self) -> DBusStr: return self._string_prop @string_prop.setter - def string_prop(self, val: 's'): + def string_prop(self, val: DBusStr): self._string_prop = val - If you use Python annotations for type hints, you can use :class:`typing.Annotated` - to specify the Python type and the D-Bus signature at the same time like this:: - - from dbus_fast.annotations import DBusStr + Originally, D-Bus signature strings were used directly in the annotations:: @dbus_property() - def string_prop(self) -> DBusStr: + def string_prop(self) -> 's': return self._string_prop @string_prop.setter - def string_prop(self, val: DBusStr): + def string_prop(self, val: 's'): self._string_prop = val + Such usage is now deprecated and support will be removed in the future. + + .. versionchanged:: v4.1.0 + String annotations are deprecated and will raise a warning. Use + :class:`typing.Annotated` with the appropriate annotation from + :mod:`dbus_fast.annotations` instead. + .. versionchanged:: v4.0.0 :class:`typing.Annotated` can now be used to provide type hints and the D-Bus signature at the same time. Older versions require D-Bus signature diff --git a/tests/client/test_methods.py b/tests/client/test_methods.py index 90636020..f25095e2 100644 --- a/tests/client/test_methods.py +++ b/tests/client/test_methods.py @@ -7,7 +7,7 @@ import sys from logging.handlers import QueueHandler from queue import SimpleQueue -from typing import Annotated, no_type_check +from typing import Annotated, Any, no_type_check import pytest @@ -22,6 +22,19 @@ has_gi = check_gi_repository() +def deprecated_dbus_method(): + inner_wrapper = dbus_method() + + def outer_wrapper(*args: Any) -> Any: + with pytest.warns( + DeprecationWarning, + match=r"String annotations are deprecated.*Use typing\.Annotated.*instead.", + ): + return inner_wrapper(*args) + + return outer_wrapper + + class ExampleInterface(ServiceInterface): def __init__(self): super().__init__("test.interface") @@ -36,7 +49,7 @@ def EchoInt64(self, what: DBusInt64) -> DBusInt64: # This one intentionally keeps string-style annotations for coverage purposes. @no_type_check - @dbus_method() + @deprecated_dbus_method() def EchoString(self, what: "s") -> "s": return what diff --git a/tests/service/test_methods.py b/tests/service/test_methods.py index 64d51c22..8fb00cfc 100644 --- a/tests/service/test_methods.py +++ b/tests/service/test_methods.py @@ -3,7 +3,7 @@ from __future__ import annotations import asyncio -from typing import Annotated, no_type_check +from typing import Annotated, Any, no_type_check import pytest @@ -21,6 +21,19 @@ from dbus_fast.service import ServiceInterface, dbus_method +def deprecated_dbus_method(): + inner_wrapper = dbus_method() + + def outer_wrapper(*args: Any) -> Any: + with pytest.warns( + DeprecationWarning, + match=r"String annotations are deprecated.*Use typing\.Annotated.*instead.", + ): + return inner_wrapper(*args) + + return outer_wrapper + + class ExampleInterface(ServiceInterface): def __init__(self, name: str) -> None: super().__init__(name) @@ -32,7 +45,7 @@ def echo(self, what: DBusStr) -> DBusStr: # This one intentionally keeps string-style annotations for coverage purposes. @no_type_check - @dbus_method() + @deprecated_dbus_method() def echo_multiple(self, what1: "s", what2: "s") -> "ss": # noqa: UP037 assert type(self) is ExampleInterface return what1, what2