Skip to content

Commit

Permalink
allow Annotated as type annotation
Browse files Browse the repository at this point in the history
  • Loading branch information
ldet authored and garyvdm committed May 10, 2024
1 parent e5c2653 commit 172cfb3
Show file tree
Hide file tree
Showing 4 changed files with 210 additions and 3 deletions.
21 changes: 20 additions & 1 deletion dbus_next/_private/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,20 @@ def _replace(idx):
return body


def parse_annotation(annotation: str) -> str:
def select_annotated_metadata(annotation) -> str:
'''
An PEP 593 Annotated instance (and the typing_extensions versions of that)
may contain multiple types of metadata, designed to be used by multiple
different libraries. To support that we only select string constants from
the annotation metadata and ignore others
'''
for meta_annotation in annotation.__metadata__:
if type(meta_annotation) is str:
return meta_annotation
raise ValueError("service annotation using PEP 593 Annotated must contain a string constant")


def parse_annotation(annotation) -> str:
'''
Because of PEP 563, if `from __future__ import annotations` is used in code
or on Python version >=3.10 where this is the default, return annotations
Expand All @@ -106,6 +119,12 @@ def raise_value_error():

if not annotation or annotation is inspect.Signature.empty:
return ''

# checking with hasattr because th python 3.6 version of
# typing_extensions.Annotated does not support isinstance
if hasattr(annotation, "__metadata__"):
annotation = select_annotated_metadata(annotation)

if type(annotation) is not str:
raise_value_error()
try:
Expand Down
10 changes: 8 additions & 2 deletions docs/high-level-service/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ If any file descriptors are sent or received (DBus type ``h``), the variable ref
method, dbus_property, signal)
from dbus_next import Variant, DBusError
# Optional:
from typing_extensions import Annotated
# from typing import Annotated # Python >= 3.9
import asyncio
class ExampleInterface(ServiceInterface):
Expand Down Expand Up @@ -64,12 +68,14 @@ If any file descriptors are sent or received (DBus type ``h``), the variable ref
def Changed(self) -> 'b':
return True
# Alternative type hint using Annotated.
# Requires either Python 3.9 or typing-extensions
@dbus_property()
def Bar(self) -> 'y':
def Bar(self) -> Annotated[int, 'y']:
return self._bar
@Bar.setter
def Bar(self, val: 'y'):
def Bar(self, val: Annotated[int, 'y']):
if self._bar == val:
return
Expand Down
162 changes: 162 additions & 0 deletions test/service/test_decorators_annotated.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
from typing import List
from dbus_next import PropertyAccess, introspection as intr
from dbus_next.service import method, signal, dbus_property, ServiceInterface

from test.util import check_annotated, skip_reason_no_typing_annotated

import pytest

has_annotated = check_annotated()

if has_annotated:
from test.util import Annotated

class ExampleInterface(ServiceInterface):
def __init__(self):
super().__init__('test.interface')
self._some_prop = 55
self._another_prop = 101
self._weird_prop = 500

@method()
def some_method(self, one: Annotated[str, 's'], two: Annotated[str,
's']) -> Annotated[str, 's']:
return 'hello'

@method(name='renamed_method', disabled=True)
def another_method(self, eight: Annotated[str, 'o'], six: Annotated[str, 't']):
pass

@signal()
def some_signal(self) -> Annotated[List[str], 'as']:
return ['result']

@signal(name='renamed_signal', disabled=True)
def another_signal(self) -> Annotated[List, '(dodo)']:
return [1, '/', 1, '/']

# an additional annotation is added to this one. PEP 593 says we shoould be
# able to deal with that and only select the annotation that we understand
@dbus_property(name='renamed_readonly_property', access=PropertyAccess.READ, disabled=True)
def another_prop(self) -> Annotated[int, 42, 't']:
return self._another_prop

@dbus_property()
def some_prop(self) -> Annotated[int, 'u']:
return self._some_prop

@some_prop.setter
def some_prop(self, val: Annotated[int, 'u']):
self._some_prop = val + 1

# for this one, the setter has a different name than the getter which is a
# special case in the code
@dbus_property()
def weird_prop(self) -> Annotated[int, 't']:
return self._weird_prop

@weird_prop.setter
def setter_for_weird_prop(self, val: Annotated[int, 't']):
self._weird_prop = val


@pytest.mark.skipif(not has_annotated, reason=skip_reason_no_typing_annotated)
def test_method_decorator():
interface = ExampleInterface()
assert interface.name == 'test.interface'

properties = ServiceInterface._get_properties(interface)
methods = ServiceInterface._get_methods(interface)
signals = ServiceInterface._get_signals(interface)

assert len(methods) == 2

method = methods[0]
assert method.name == 'renamed_method'
assert method.in_signature == 'ot'
assert method.out_signature == ''
assert method.disabled
assert type(method.introspection) is intr.Method

method = methods[1]
assert method.name == 'some_method'
assert method.in_signature == 'ss'
assert method.out_signature == 's'
assert not method.disabled
assert type(method.introspection) is intr.Method

assert len(signals) == 2

signal = signals[0]
assert signal.name == 'renamed_signal'
assert signal.signature == '(dodo)'
assert signal.disabled
assert type(signal.introspection) is intr.Signal

signal = signals[1]
assert signal.name == 'some_signal'
assert signal.signature == 'as'
assert not signal.disabled
assert type(signal.introspection) is intr.Signal

assert len(properties) == 3

renamed_readonly_prop = properties[0]
assert renamed_readonly_prop.name == 'renamed_readonly_property'
assert renamed_readonly_prop.signature == 't'
assert renamed_readonly_prop.access == PropertyAccess.READ
assert renamed_readonly_prop.disabled
assert type(renamed_readonly_prop.introspection) is intr.Property

weird_prop = properties[1]
assert weird_prop.name == 'weird_prop'
assert weird_prop.access == PropertyAccess.READWRITE
assert weird_prop.signature == 't'
assert not weird_prop.disabled
assert weird_prop.prop_getter is not None
assert weird_prop.prop_getter.__name__ == 'weird_prop'
assert weird_prop.prop_setter is not None
assert weird_prop.prop_setter.__name__ == 'setter_for_weird_prop'
assert type(weird_prop.introspection) is intr.Property

prop = properties[2]
assert prop.name == 'some_prop'
assert prop.access == PropertyAccess.READWRITE
assert prop.signature == 'u'
assert not prop.disabled
assert prop.prop_getter is not None
assert prop.prop_setter is not None
assert type(prop.introspection) is intr.Property

# make sure the getter and setter actually work
assert interface._some_prop == 55
interface._some_prop = 555
assert interface.some_prop == 555

assert interface._weird_prop == 500
assert weird_prop.prop_getter(interface) == 500
interface._weird_prop = 1001
assert interface._weird_prop == 1001
weird_prop.prop_setter(interface, 600)
assert interface._weird_prop == 600


@pytest.mark.skipif(not has_annotated, reason=skip_reason_no_typing_annotated)
def test_interface_introspection():
interface = ExampleInterface()
intr_interface = interface.introspect()
assert type(intr_interface) is intr.Interface

xml = intr_interface.to_xml()

assert xml.tag == 'interface'
assert xml.attrib.get('name', None) == 'test.interface'

methods = xml.findall('method')
signals = xml.findall('signal')
properties = xml.findall('property')

assert len(xml) == 4
assert len(methods) == 1
assert len(signals) == 1
assert len(properties) == 2
20 changes: 20 additions & 0 deletions test/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,23 @@ def check_gi_repository():
except ImportError:
_has_gi = False
return _has_gi


_has_annotated = False
import typing
if hasattr(typing, "Annotated"):
from typing import Annotated
_has_annotated = True
else:
try:
from typing_extensions import Annotated
_has_annotated = True
except ImportError:
pass

skip_reason_no_typing_annotated = 'Annotated tests require python 3.9 or typing-extensions'


def check_annotated():
global _has_annotated
return _has_annotated

0 comments on commit 172cfb3

Please sign in to comment.