-
-
Notifications
You must be signed in to change notification settings - Fork 21.3k
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
Allow setters and getters to take additional arguments #78991
base: master
Are you sure you want to change the base?
Conversation
6cd0aac
to
c33b085
Compare
|
c33b085
to
da4c810
Compare
Done! Thanks for the help :) |
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.
1. Here you also need to add the check.
2. After changing the error messages, the tests fail, you must also update the text in them.
Log
modules/gdscript/tests/gdscript_test_runner.cpp:213: ERROR: CHECK( result.passed ) is NOT correct!
values: CHECK( false )
logged: modules/gdscript/tests/scripts/analyzer/errors/setter_with_wrong_value_arg.gd
GDTEST_ANALYZER_ERROR
Value argument of type "Array" in setter "set_named" is not compatible with a property of type "int".
modules/gdscript/tests/gdscript_test_runner.cpp:213: ERROR: CHECK( result.passed ) is NOT correct!
values: CHECK( false )
logged: modules/gdscript/tests/scripts/analyzer/errors/property_function_get_type_error.gd
GDTEST_ANALYZER_ERROR
Return type of getter "get_prop" is not compatible with a property of type "int".
modules/gdscript/tests/gdscript_test_runner.cpp:213: ERROR: CHECK( result.passed ) is NOT correct!
values: CHECK( false )
logged: modules/gdscript/tests/scripts/analyzer/errors/property_function_set_type_error.gd
GDTEST_ANALYZER_ERROR
Value argument of type "int" in setter "set_prop" is not compatible with a property of type "String".
modules/gdscript/tests/gdscript_test_runner.cpp:213: ERROR: CHECK( result.passed ) is NOT correct!
values: CHECK( false )
logged: modules/gdscript/tests/scripts/analyzer/errors/setter_with_wrong_name_arg.gd
GDTEST_ANALYZER_ERROR
First mandatory argument of setter "set_named" with 2 mandatory arguments should be a property name and must be a String or StringName.
3. Probably, tests should be duplicated for static variables, since their implementation is also largely duplicated. This does not please me, but I think it makes sense, taking into account point 1.
I have some questions about a few details.
Callable::CallError GDScript::call_getter(const MemberInfo &p_member, const StringName &p_name, Variant &r_ret) const {
Callable::CallError ce;
if (p_member.getter_with_name) {
Variant property_name = p_name;
const Variant *args = &property_name;
r_ret = const_cast<GDScript *>(this)->callp(p_member.getter, &args, 1, ce);
} else {
r_ret = const_cast<GDScript *>(this)->callp(p_member.getter, nullptr, 0, ce);
}
return ce;
} Where should I stick this method in the class? Should it be private? public? protected? Do I need to make a copy of this method for |
No, I think setters/getters with optional arguments are handled correctly. 1. Optional arguments are not required to be passed when called, so this works. 2. If you want to pass optional arguments, then you should call the setter/getter function instead: my_var = value # The syntax itself doesn't allow additional arguments.
set("my_var", value) # The core doesn't support additional arguments.
set_my_var(value, opt_arg_1, opt_arg_2) # It works. In fact, #74144 reports the issue that you cannot use such functions as setters/getters. But we're not talking about setters/getters supporting extra arguments. Just now you can combine these two things in one function.
Yes, for |
That makes total sense about the optional arguments, thanks for explaining that :) I'll be honest, I'm kind of scared of macros, so I'll just stick to functions :). One thing I thought of for the Thanks! |
Apologies for the multiple commits. I'm working on rebasing them into 1 commit right now. |
7f90290
to
5555d51
Compare
Some of my changes got screwed up when I squashed. Thankfully I made a backup branch :) This last commit should be good to go now! |
My bad, forgot to add tests for static variables. I'll write those now. |
782ecbd
to
53120ff
Compare
I ran the GDScript tests properly now instead of manually filling in the .out files, so the CI should no longer complain about getting the wrong error messages in the test with this latest commit. |
modules/gdscript/tests/scripts/runtime/features/setter_getter_with_name.gd
Outdated
Show resolved
Hide resolved
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.
- Some of the old comments (about the
static_member
name, about the removal of blank lines) have not yet been addressed. - There also seems to be a lack of error tests for too many or too few mandatory parameters of setter/getter functions.
Thanks for the review again! Sorry about forgetting those old comments. I'll try getting into the habit of leaving Todos for myself as soon as I read a review. Good point about the error tests, I'll add those in. |
53120ff
to
6cc2ef4
Compare
I think I've addressed all the comments now except for the minor change in logic related to the getter always returning true versus only returning true if there was no call error. |
6cc2ef4
to
b50a33e
Compare
Finally had some time to make the requested changes! Hopefully the PR is ready to ship now :) |
Darn, co close to passing the CI... I don't really understand the error message. Any tips on how to fix? |
It's a sporadic error, unrelated to PR |
Passed!! 🥳 |
b50a33e
to
6f10a87
Compare
6f10a87
to
2781f8d
Compare
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.
The code looks good to me (but I haven't tested it again). There are only 2 points left in which we cannot reach consensus over several iterations:
- Error messages. Don't underestimate this, they are important to users. I think my suggestions are consistent, but you don't apply them consistently.
- Inconsistent error handling when calling setters/getters in
gdscript.cpp
. I'm not asking you to fix it in this PR, you could leave it as it is if you didn't change the logic (by wrappingreturn
intoif
). But since you're changing this, I suggest an option that's closer to GDScript: Fix some methods still can use multi-level calls #81139.
I really like this PR. I hope we reach a consensus soon!
Callable::CallError ce = call_setter(*static_member, p_name, value); | ||
if (ce.error == Callable::CallError::CALL_OK) { | ||
return true; // TODO: should this just be return ce.error == Callable::CallError::CALL_OK? | ||
} |
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.
Callable::CallError ce = call_setter(*static_member, p_name, value); | |
if (ce.error == Callable::CallError::CALL_OK) { | |
return true; // TODO: should this just be return ce.error == Callable::CallError::CALL_OK? | |
} | |
call_setter(*static_member, p_name, value); // TODO: returned call error not used. | |
return true; |
Callable::CallError ce = call_setter(*member, p_name, value); | ||
if (ce.error == Callable::CallError::CALL_OK) { | ||
return true; // TODO: should this just be return ce.error == Callable::CallError::CALL_OK? | ||
} |
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.
Callable::CallError ce = call_setter(*member, p_name, value); | |
if (ce.error == Callable::CallError::CALL_OK) { | |
return true; // TODO: should this just be return ce.error == Callable::CallError::CALL_OK? | |
} | |
call_setter(*member, p_name, value); // TODO: returned call error not used. | |
return true; |
Callable::CallError ce = call_setter(*static_member, p_name, value); | ||
if (ce.error == Callable::CallError::CALL_OK) { | ||
return true; // TODO: should this just be return ce.error == Callable::CallError::CALL_OK? |
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.
Callable::CallError ce = call_setter(*static_member, p_name, value); | |
if (ce.error == Callable::CallError::CALL_OK) { | |
return true; // TODO: should this just be return ce.error == Callable::CallError::CALL_OK? | |
call_setter(*static_member, p_name, value); // TODO: returned call error not used. | |
return true; |
Callable::CallError ce = call_getter(*member, p_name, r_ret); | ||
if (ce.error == Callable::CallError::CALL_OK) { | ||
return true; // TODO: should this just be return ce.error == Callable::CallError::CALL_OK? | ||
} |
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.
Callable::CallError ce = call_getter(*member, p_name, r_ret); | |
if (ce.error == Callable::CallError::CALL_OK) { | |
return true; // TODO: should this just be return ce.error == Callable::CallError::CALL_OK? | |
} | |
call_getter(*member, p_name, r_ret); // TODO: returned call error not used. | |
return true; |
Callable::CallError ce = sptr->call_getter(*static_member, p_name, r_ret); | ||
if (ce.error == Callable::CallError::CALL_OK) { | ||
return true; // TODO: should this just be return ce.error == Callable::CallError::CALL_OK? | ||
} |
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.
Callable::CallError ce = sptr->call_getter(*static_member, p_name, r_ret); | |
if (ce.error == Callable::CallError::CALL_OK) { | |
return true; // TODO: should this just be return ce.error == Callable::CallError::CALL_OK? | |
} | |
sptr->call_getter(*static_member, p_name, r_ret); // TODO: returned call error not used. | |
return true; |
GDScriptParser::DataType name_arg_type = getter_function->parameters[0]->get_datatype(); | ||
if (!name_arg_type.is_variant() && name_arg_type.is_hard_type()) { | ||
if (name_arg_type.kind != GDScriptParser::DataType::BUILTIN || (name_arg_type.builtin_type != Variant::STRING && name_arg_type.builtin_type != Variant::STRING_NAME)) { | ||
push_error(R"(First mandatory argument of a getter must be a property name of type String or StringName.)", member.variable); |
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.
push_error(R"(First mandatory argument of a getter must be a property name of type String or StringName.)", member.variable); | |
push_error(R"(First mandatory argument of a getter is a property name and must be of type String or StringName.)", member.variable); |
has_valid_setter = true; | ||
|
||
if (setter_function->mandatory_parameter_count == 0 || setter_function->mandatory_parameter_count > 2) { | ||
push_error(vformat(R"(Setters must have 1 or 2 mandatory arguments.)"), member.variable); |
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.
push_error(vformat(R"(Setters must have 1 or 2 mandatory arguments.)"), member.variable); | |
push_error(vformat(R"(A setter must have 1 or 2 mandatory arguments.)"), member.variable); |
For consistency with the getter error message.
} else { | ||
GDScriptParser::ParameterNode *value_arg = setter_function->parameters[setter_function->mandatory_parameter_count - 1]; | ||
if (!is_type_compatible(member.variable->datatype, value_arg->datatype, true)) { | ||
push_error(vformat(R"(Setter argument "%s" is not compatible with variable type "%s".)", value_arg->identifier->name, member.variable->datatype.to_string()), member.variable); |
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.
The argument type is incompatible, not the argument itself. This is important information. You can remove all other names, but the argument type and variable type must be present in the error message, in my opinion.
My previous suggestion:
push_error(vformat(R"(Setter argument "%s" is not compatible with variable type "%s".)", value_arg->identifier->name, member.variable->datatype.to_string()), member.variable); | |
push_error(vformat(R"(Type "%s" of argument "%s" in setter "%s" is not compatible with type "%s" of property "%s".)", value_arg->datatype.to_string(), value_arg->identifier->name, setter_function->identifier->name, member.variable->datatype.to_string(), member.variable->identifier->name), member.variable); |
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.
Lol, I think we've come full circle :)
I think that's how I wrote the error message before, but we wanted the error message to be shorter.
How about Setter argument "%s" of type "%s" is not compatible with variable type "%s"
?
Side note: should I change all the instances of "not compatible" to "incompatible" to save a few characters?
GDScriptParser::DataType name_arg_type = setter_function->parameters[0]->datatype; | ||
if (!name_arg_type.is_variant() && name_arg_type.is_hard_type()) { | ||
if (name_arg_type.kind != GDScriptParser::DataType::BUILTIN || (name_arg_type.builtin_type != Variant::STRING && name_arg_type.builtin_type != Variant::STRING_NAME)) { | ||
push_error(R"(First mandatory argument of a setter with 2 mandatory arguments must be of type String or StringName.)", member.variable); |
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.
push_error(R"(First mandatory argument of a setter with 2 mandatory arguments must be of type String or StringName.)", member.variable); | |
push_error(R"(First argument of a setter with 2 mandatory arguments must be of type String or StringName.)", member.variable); |
Or this, for consistency with getter:
push_error(R"(First mandatory argument of a setter with 2 mandatory arguments must be of type String or StringName.)", member.variable); | |
push_error(R"(First argument of a setter with 2 mandatory arguments is a property name and must be of type String or StringName.)", member.variable); |
Please either add it here or remove it there.
Thanks for all the help :) I'm excited to start using this feature in my projects! |
* Add support for name argument. * Allow optional arguments. Co-authored-by: dalexeev <[email protected]>
2781f8d
to
335239d
Compare
@adamscott Recently brought up whether the signature for a setter should require the property name to be the first or second argument. The more I think about it, the more it makes sense for me that the property name should be the second argument. I was at first thinking that it would make sense to have the property name come first, so that users could specify a default value for the setter, but that actually wouldn't work because the setter must have exactly 2 mandatory arguments for the name to be passed in to the argument. I can modify the PR with the new signature if we agree that is the way to go. |
I don't think it makes sense, since the setter/getter type (normal vs named) is determined by the number of manatory parameters (you can use optional parameters as it is allowed in Godot 3, but they are not taken into account at all). This might make sense if you wanted to have one of the arguments optional, but I don't think it matters compared to the problem below. This hasn't been allowed before anyway, so it doesn't break compatibility. In my opinion, the order "value, name" doesn't make sense, it's not what you would intuitively expect and contradicts the rest of the Godot APIs ( func set_property(value, name):
_data.set(value, name) |
That is a really good point dalexeev. Let's keep the signature as is then. Less work for me too :) |
Updated the OP with an example to clarify the new feature. Let me know if there's any thing else I can help with to get the PR across the finish line :) |
I personally don't like how this feature is meant to work from UX perspective, i dont think the amount of boilerplate is reduced.
and then you dont need any setter or getter at all |
Thanks for the comment MarianoGnu! Your idea is very similar to what I originally proposed a few months ago: godotengine/godot-proposals#6750. The current approach is a compromise that doesn't achieve quite the same level of boilerplate reduction, but is also much more flexible and aligned with GDScript's current syntax. For example, what if you want the proxy property to emit a signal when it changes but the original property doesn't emit a signal? The @export_proxy_property approach doesn't provide a clean way to do this, whereas the setter/getter approach does. |
Please see the original pull request #76665 for summary of changes and discussion.
Edit 2023/Sept/18 8:43: Copied some of the examples over from the linked PR for convenience and expanded on them a bit.
Examples
addresses the following proposals:
@observable
annotation for properties in GDScript godot-proposals#4867.Bugsquad edit: Add lists for issues/PRs links.