-
Notifications
You must be signed in to change notification settings - Fork 76
Support automatic XML introspection generation #29
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
It's hard to say if it's a good idea or not. In most cases, the DBus Introspection XML is a part of a pre-existing standard (like MPRIS), and the programmer can simply copy the XML file without needing to worry how to exactly replicate its behavior in his Python code. I guess we could also provide a object validator that automatically checks if all required methods are implemented, and that they take correct number of arguments; or even a XML to Python skeleton converter for quick starting implementation. However, when there is no pre-existing standard, and the programmer is creating a new one, it can be quite easier to write everything in Python, and automatically generate XML data - as XML is not really a nice format to write by hand. Also, having the argument data types specified in the Python source code might be useful for its readability. In fact, we could automatically test if the parameters have correct types even when methods are not called through DBus, but from other Python code - which might be useful for autotests, and for objects that are both published on DBus and used internally by the application. This suggests that type-related decorators could be implemented as a generic, non-dbus related tool, like signals in pydbus.generic. But we don't always need type decorators. Without them, we can generate a skeleton XML file - listing all the methods and their argument names; but specyfing their arguments as "TODO" - to help programmer with writing the XML file. I guess, we can split this feature into two smaller ones - the one about generic type-related decorators; and the one about generating DBus XML from a given Python class. And the first one should expose metadata that will let the second one automatically fill out the TODOs, and let the user publish the object without touching the XML at all. Both are useful and self-contained, and together they have synergy. As an added bonus, developers will be able to quickly change from using autogenerated XML to using a XML file when they decide to freeze the API and publish it as a standard; without a need to remove type decorators, as they're still a useful part of the code. |
OK, so now back to your pull request:
Note that it should work in the same way as
|
Okay I'll look more closely through your suggestions but thanks for your response. I have fixed the tab/spaces issue, so future commits will be fixed (I'm going to change this commit here too). Regarding And regarding the weakly typed methods and properties (point 3), Also |
@Signalled - right, I've forgotten about this annotation. So this is necessary, but I still don't think that it should send the signal automatically. And probably should be used on the getter, not the setter. @dbus_property/method - I don't really understand. I think we can safely assume that all names starting from an uppercase letter are public, as normal python names start from lowercase letters, and everything magical starts from _. If somebody adds a new method starting with an uppercase letter, he probably wants to make it public; and if not, he's writing a really confusing piece of code. About gen_introspection - yup, I know :D |
But to which interface does the method or property belong? With
This doesn't mean that it must be mandatory. Via Regarding checking the types I first was thinking whether it could infer the types from the signature:
But this is afaik not really possible, as I just remember that I wanted to add “autocomplete” features (e.g. if the interface is |
Objects implementing multiple interfaces are a bit of a complicated case. There are cases like MPRIS2, where all methods are grouped into 4 interfaces, and 3 of them are extensions; and there are cases where they're used for versioning, or dynamically added and removed based on some events (like a optical drive, that adds an interface when a CD is inserted). In the MPRIS case, I guess annotating decorators are the way to go. The other two cases aren't currently supported by pydbus in any sane way, so they're out of scope here. So, we have two ways to specify MPRIS's ActivatePlaylist method:
I feel the second one is prettier, as we're stating two different facts about the method; and the "at interface" sounds semantically well. The @interface decorator of course can be used also for properties and signals. However, while thinking about it more, I've found another possibility:
And this is the cleanest option. However: Python 2 does not support annotations :D But while we could provide a Python 2 fallback (2nd option, with @typed_method renamed to @annotate_method, and without type checking), this is a place where we're clearly using a Python 3 feature, and we can direct Python 2 users to the existing, XML file based method of specifying interfaces. And I feel there're already too many hacks for Python 2 in pydbus, and it's time to give people reasons to migrate to Python 3. In the future, @TypeChecked decorator could be added to verify the annotations at runtime, but it's not really required now, and outside of scope of this feature. Also, as it's really easy to annotate methods this way, I don't think that require_strong_typing=False is useful anymore - we can always throw an exception when a unannotated method is found. |
(Of course for methods on the default interface, @interface is not required.) |
Okay I'm currently working on implementing your suggestions. One question I have is, what do you mean with |
Okay I didn't noticed you had answered in the mean time, so I have actually worked on using decorators and not annotations. I personally prefer that route, as it won't break Python 2.7 (which is still widely used) and I don't think it is that much of a overhead. As I like your second suggestion, just using one |
About typed_signal - objects can send signals, and currently that's being defined with from pydbus.generic import signal
class X:
SignalName = signal() To autogenerate XML for them, we need a typed_signal that takes the types of signal's arguments. |
Ah okay you are right. But that might be a bit more complicated as you cannot annotate assignments, so it would be something like this: class X:
SignalName = typed_signal(TYPES) (having to call I have applied (probably) all of your suggestions except using annotations, but hopefully I'll have that today. (No guarantee regarding |
That's why I haven't suggested typed_signal as a decorator :D I didn't write "@" before its name :D Probably it should be a class that inherits pydbus.generic.signal, and has a different constructor. |
Yeah I noticed that. I just wanted to make sure we are on the same page regarding it not being a decorator (especially if you consider that everything else are basically decorators). |
Okay I have now uploaded the changes I did. It currently has also some support for signals but there are still a few questions remaining things. For one you used lowercase names (like I also haven't added any tests for the annotations yet, as they cannot be used together with Python 2, so they need to be separate files. Also the |
@@ -265,7 +255,7 @@ in a ''dbus'' class property or in its ''docstring''. For example:: | |||
def SomeProperty(self): | |||
return self._someProperty | |||
|
|||
@SomeProperty.setter | |||
@signalled(SomeProperty) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this should work this way instead:
@emits_changed_signal
@property
def SomeProperty(self):
...
@SomeProperty.setter
def SomeProperty(self, value):
...
Especially as a signal might be sent even for read-only properties; and it's absolutely useless for write-only ones.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How do signals on read-only properties work? When they are changed internally?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For example, our object is a "CD drive", and our property is "DiskAvailable". User presses the physical eject button, and kernel ejects the drive. Our daemon detects that fact, and emits a signal.
@@ -34,3 +35,10 @@ def decorate(func): | |||
verify_arguments(func) | |||
return func | |||
return decorate | |||
|
|||
|
|||
class TypedSignal(signal): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've used "signal" and not "Signal" to be consistent with "property" (which is a class), "function" and "method" (which are not exported, but can be found by type(A.method) and type(A().method)).
Signals, just like properties, look more like language features, and less like normal classes (and are language features in languages like C++/Qt). Python's CamelCase convention for class names is used mostly for domain-specific things, and builtin types (int, str, etc) and syntax-extension-wannabes (property, classmethod, etc) don't follow it.
Also, as we have a typed_method and a typed_property, TypedSignal looks quite strange.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't it be typedsignal
(like staticmethod
) by that logic?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's a good question. Following the builtins's and functools's convention, it should. While I feel with _
it's more readable, we shouldn't break the rule.
So yeah, the correct names are typedproperty
, typedmethod
and typedsignal
.
|
||
class TypedSignal(signal): | ||
|
||
def __init__(self, arguments): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it should be *arguments, as
SomethingHappened = typed_signal("s", "i")
is a more obvious thing to write than
SomethingHappened = typed_signal(("s", "i"))
Especially in the case of single-argument signals.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well it actually needs to be a (name, type)
tuple, so in varargs it still would be typed_signal(("arg1", "s"), ("arg2", "i"))
. And kwargs won't work as the order is not defined. Also I actually didn't use varargs to be in line with typed_method
, which doesn't use them.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not in love with typed_method's interface, but as it's only a fallback for people that can't use Python 3, I don't really care. On the other hand, typed_signal should be pretty.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay what would you suggest? typed_signal(("arg1", "s"), ("arg2", "i"))
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm... Realistic example would look like that:
PropertiesChanged = typed_signal(("interface_name", "s"), ("changed_properties", "a{sv}"), ("invalidated_properties", "as"))
Not the prettiest thing ever, but I don't think we can find anything better with this syntax.
However, this one looks awesome (and Python3-only, so the previous solution is useful as a fallback):
@signal
def PropertiesChanged(interface_name: 's', changed_properties: 'a{sv}', invalidated_properties: 'as'):
pass
(Note, it can be done with pydbus.generic.signal
, there is no need for another class here)
(2nd note: Sorry for constantly changing my opinion :D I can add the prettier syntax myself after merging the patch if you prefer :D)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You last suggestion is fascinating! In theory you could define signals as methods (irrespective of this patch), and then you could either use the Python 3 syntax or @typed_method
to specify the parameter types.
So in theory @typed_signal
doesn't need to be implemented at all. I'll try to look at that feature because then it doesn't need to introduce something that isn't necessary actually.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We still need to know if this function is a method or a signal.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well for that there is the @signal
decorator. For example:
# In Python 3
@signal
def PropertiesChanged(interface_name: 's', changed_properties: 'a{sv}', invalidated_properties: 'as'):
pass
# In Python 2
@signal
@typed_method(("s", "a{sv}", "as"), None)
def PropertiesChanged(interface_name, changed_properties, invalidated_properties):
pass
Now in both cases the decorator will store the method inside the signal
class itself. And if the introspector comes along and sees a signal
class it can access that method object and then do the almost same stuff it'd do to a normal method. The only difference is that it won't allow a return type (as far as I understand it that isn't a thing in D-Bus) and that no argument has a direction defined (or if it were it should be out
not in
).
As a matter of fact I have actually already implemented it that way and just finished with cleaning it up. So if you wanted I could upload it soonish. (As I write this I noticed that I haven't verified that @interface
behaves as expected)
The only ugly thing there is the superfluous pass
block. But I think apart from that it is actually quite nice.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Awesome.
BTW, I've initially forgotten about self, but it probably should be here too, as this isn't a static method.
raise ValueError( | ||
"Default values are not allowed for method " | ||
"'{}'".format(function.__name__)) | ||
# TODO: Check if vargs or kwargs are actually allowed in dbus. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, they aren't.
However, you should skip arguments with names starting from "dbus_", as "dbus_context" might be passed as a keyword argument automatically by pydbus: 78daafa
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah okay thanks for the information (both that vargs and kwargs are not allowed and that you have dbus_context
). But do I see it correctly that this dbus_context
has nothing to do with vargs
or kwargs
? If a method has keyword arguments it won't populate dbus_context
would it? (But nevertheless it should skip those arguments)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, dbus_context won't get passed if somebody is using **kwargs. But we need to handle the case when it is stated explicitely in the function's signature (like def MyMethod(self, sth1, sth2, dbus_context=None):)
(GitHub collapsed one of two TypedSignal-related comments as outdated, you might need to click "Show outdated") |
If the `signal` class is used as a decorator it'll use the decorated function as the name and also cache the decorated method.
It enables to automatically inspect a class and generate XML introspection data by looking through each method, signal and property. As the types are not defined it won't add them to the data.
The decorators add information about the types a method or property can accept or return. The XML generator can optionally require that every method and property has the information defined. Alternatively it adds the possibility to use annotations instead, but that is a Python 3 only feature.
Regarding the |
Ok, I know this may be too late, but you could use the new type hints with python2.7 in the same file. All you have to do is move them to a single line comment beneath the function signature (see here). But I don't really know if there is an equivalent for |
This patch supports type hints using the Python 3 syntax but there is no such support for Python 2. One major reason is that it would have to actually parse the file itself, because (as far as I can tell) the annotations using comments aren't actually parsed by Python. |
Hi, how does it look with this pull request? We would like to use this approach in anaconda. |
We have implemented our own solution with type hints and we heavily depend on it in our project. This implementation is now part of the dasbus library (https://github.com/rhinstaller/dasbus) that we have started to use instead of pydbus. |
This enables a class to generate automatic XML introspection data. This is probably not the final version, especially as I noticed that you use tab indentation while I use spaces (the
TypeError
exception for example is copied and thus uses tabs).But the main reason for already open this request now is to determine if this is actually needed or wanted.
I unfortunately haven't actually tested this on an actual D-Bus service as I don't actually have written one yet. I'll probably test that as well when there is interest.