1768 .NET Attribute Support: Fix#2032
Conversation
- Added support for specifying .NET attributes using __clr_attributes__. Classes, properties and methods can specify this value. - Added support for Attribute shorthand without the "Attribute" ending. - Added unit test verifying the behavior
| Type attribute = tp[0].As<Type>(); | ||
| if (typeof(Attribute).IsAssignableFrom(attribute) == false) | ||
| { | ||
| throw new Exception($"This type cannot be used as an attribute type: {attribute}"); |
There was a problem hiding this comment.
The exception type must be specific.
Also, please add a test for this scenario to ensure it does not lead to a fatal error. You might need to raise a Python exception instead.
| // in python there is no difference. But luckily there is rarely a conflict, so we can treat | ||
| // named arguments and named properties or fields the same way. | ||
| var tp = new PyTuple(attr); | ||
| Type attribute = tp[0].As<Type>(); |
There was a problem hiding this comment.
Same as below, you need to handle the scenario where the object passed here is not actually a Type properly.
| var args = tp[1].As<PyObject[]>(); | ||
| var dict = new PyDict(tp[2]); |
| var dict = new PyDict(tp[2]); | ||
| var dict2 = new Dictionary<string, PyObject>(); |
There was a problem hiding this comment.
Please name variables with descriptive names.
| var allConstructors = attribute.GetConstructors(); | ||
| foreach (var constructor in allConstructors) |
There was a problem hiding this comment.
This reimplements our overloading logic. We definitely do not want to do that. Instead the Python side should use some simple mechanism to set the fields. For example, there could be a .with(attributes: dict[str, Any]) -> Self method, that would set values of the fields/properties from the dictionary.
E.g. DllImport("kernel32").with({"CharSet": Auto}) (just an example, a different way to do this could also be acceptable).
There was a problem hiding this comment.
I think that might make sense for properties. I just wanted the call to be analogous to how it is invoked in C#, since people will always be finding the C# documentation/ code samples for how those attributes are used. e.g
[AttributeTest("UnnamedArg", NamedArg : "x", NamedProperty="y")]This would now be used like this:
AttributeTest("UnnamedArg", NamedArg = "x", NamedProperty="y")But you are suggesting this instead?
AttributeTest("UnnamedArg", NamedArg = "x").with({"NamedProperty": "y"})There was a problem hiding this comment.
@rmadsen-ks correct. The reason is that we don't want to maintain yet another overload resolution handler. That's one of the most complicated parts of C# that we try to mirror.
There was a problem hiding this comment.
Ok, so you are not talking so much about changing these specific lines, but rather changing how the data for that is stored?
I am not sure we can use the other overload resolution handler, because that is for runtime overload resolution, right? this is compile/load time and the data we can put into those attributes is limited.
There was a problem hiding this comment.
@rmadsen-ks since this is running in Python, it is runtime.
I believe you should be able to use MethodBinder.Bind which returns a Binding.
There was a problem hiding this comment.
Unless I'm completely confused, The selected overload gets compiled to IL by System.Reflection.Emit. That is what the returned CustomAttributeBuilder is for.
There was a problem hiding this comment.
@rmadsen-ks not sure I understand this comment.
MethodBinder.Bind performs basically what your loop performs: selects the method from the list of overloads, and returns it + the arguments for it as .NET would see them (e.g. an instance of Binding), which you can then use in the CustomAttributeBuilder constructor.
I am talking about replacing the whole loop with an invocation to MethodBinder.Bind
| var qname2 = qname + "Attribute"; | ||
| var type2 = AssemblyManager.LookupTypes(qname2).FirstOrDefault(t => t.IsPublic); | ||
| if (type2 != null) | ||
| { | ||
| var str = "def {2}__AttributeBuilder(*args, **kwargs):\n" | ||
| + " import {1}\n" | ||
| + " return ({0}, args, kwargs)\n"; | ||
| str = string.Format(str, qname2, _namespace, name); | ||
| PythonEngine.Exec(str); | ||
| using var attributeData = PythonEngine.Eval(name + "__AttributeBuilder"); | ||
| var pinnedAttributeData = attributeData.NewReferenceOrNull(); | ||
| StoreAttribute(name, pinnedAttributeData.Borrow()); | ||
| return new NewReference(pinnedAttributeData); | ||
| } | ||
|
|
There was a problem hiding this comment.
This seems overly complicated. Can't you just do what the code above does for type lookup, and just add another StoreAttribute for the alias?
There was a problem hiding this comment.
So for e.g BrowsableAttribute, this generates a function which takes the arguments and stores it as a tuple along with the type name. That way we can reconstruct which arguments are used for creating the attribute when later generating the code.
The reason it needs to be a function is that whenever e.g Browsableis invoked, it needs to generate a new tuple because the arguments are not the same every time.
|
@rmadsen-ks btw generally speaking the code down at that level should not be using |
Which code do you mean specifically? This code used PyDict before I arrived.. :) |
|
Closing as the requested changes were not implemented |
Added support for specifying .NET attributes using clr_attributes. Classes, properties and methods can specify this value.
Added support for Attribute shorthand without the "Attribute" ending.
Added unit test verifying the behavior
Close #1768
Checklist
Check all those that are applicable and complete.
AUTHORSCHANGELOG