Skip to content

Commit b41f07c

Browse files
committed
Greatly improve nested (e.g. godot->python->godot) attribute&method access, add support for virtual methods in bindings
1 parent bb8b67f commit b41f07c

File tree

4 files changed

+78
-41
lines changed

4 files changed

+78
-41
lines changed

generation/bindings_templates/method.tmpl.pyx

+10-2
Original file line numberDiff line numberDiff line change
@@ -147,12 +147,20 @@ with nogil:
147147
{% macro render_method(cls, method) %}
148148
# {{ render_method_c_signature(method) }}
149149
def {{ render_method_signature(method) }}:
150-
{% if method.is_supported %}
150+
{% if method.is_virtual %}
151+
cdef Array args = Array()
152+
{% for arg in method.arguments %}
153+
args.append({{ arg.name }})
154+
{% endfor %}
155+
return Object.callv(self, "{{ method.name }}", args)
156+
{% else %}
157+
{% if method.is_supported %}
151158
{{ _render_method_cook_args(method) | indent }}
152159
{{ _render_method_call(cls, method) | indent }}
153160
{{ _render_method_destroy_args(method) | indent }}
154161
{{ _render_method_return(method) | indent }}
155-
{% else %}
162+
{% else %}
156163
raise NotImplementedError("{{method.unsupported_reason}}")
164+
{% endif %}
157165
{% endif %}
158166
{% endmacro %}

pythonscript/_godot_instance.pxi

+28-10
Original file line numberDiff line numberDiff line change
@@ -61,12 +61,28 @@ cdef api godot_bool pythonscript_instance_get_prop(
6161
const godot_string *p_name,
6262
godot_variant *r_ret
6363
) with gil:
64+
# Should look among properties added by the script and it parents,
65+
# not Godot native properties that are handled by the caller
6466
cdef object instance = <object>p_data
6567
cdef object ret
68+
cdef object field
69+
cdef str key = godot_string_to_pyobj(p_name)
6670
try:
67-
ret = getattr(instance, godot_string_to_pyobj(p_name))
68-
pyobj_to_godot_variant(ret, r_ret)
71+
field = instance.__exported[key]
72+
except KeyError:
73+
return False
74+
try:
75+
if isinstance(field, ExportedField):
76+
ret = getattr(instance, godot_string_to_pyobj(p_name))
77+
pyobj_to_godot_variant(ret, r_ret)
78+
elif isinstance(field, SignalField):
79+
# TODO: Not sure how to create a Variant::Signal from GDNative
80+
return False
81+
else:
82+
# TODO: Not sure how to create a Variant::Callable from GDNative
83+
return False
6984
return True
85+
7086
except Exception:
7187
traceback.print_exc()
7288
return False
@@ -81,11 +97,12 @@ cdef api godot_variant pythonscript_instance_call_method(
8197
) with gil:
8298
cdef godot_variant var_ret
8399
cdef object instance = <object>p_data
84-
# TODO: optimize this by caching godot_string_name -> method lookup
85-
try:
86-
meth = getattr(instance, godot_string_name_to_pyobj(p_method))
100+
cdef object fn
101+
cdef str key = godot_string_name_to_pyobj(p_method)
87102

88-
except AttributeError:
103+
# TODO: optimize this by caching godot_string_name -> method lookup
104+
fn = instance.__exported.get(key)
105+
if not callable(fn):
89106
r_error.error = godot_variant_call_error_error.GODOT_CALL_ERROR_CALL_ERROR_INVALID_METHOD
90107
gdapi10.godot_variant_new_nil(&var_ret)
91108
return var_ret
@@ -95,7 +112,7 @@ cdef api godot_variant pythonscript_instance_call_method(
95112
cdef object ret
96113
try:
97114
pyargs = [godot_variant_to_pyobj(p_args[i]) for i in range(p_argcount)]
98-
ret = meth(*pyargs)
115+
ret = fn(instance, *pyargs)
99116
r_error.error = godot_variant_call_error_error.GODOT_CALL_ERROR_CALL_OK
100117
pyobj_to_godot_variant(ret, &var_ret)
101118
return var_ret
@@ -130,10 +147,11 @@ cdef api void pythonscript_instance_notification(
130147
# TODO: cache the methods to call ?
131148
for parentcls in instance.__class__.__mro__:
132149
try:
133-
# TODO: Should only call _notification for class inheriting `bindings.Object` ?
134-
parentcls.__dict__["_notification"](instance, p_notification)
135-
except (KeyError, NotImplementedError):
150+
fn = parentcls.__exported["_notification"]
151+
except (AttributeError, KeyError):
136152
pass
153+
else:
154+
fn(instance, p_notification)
137155

138156

139157
# Useful ?

pythonscript/_godot_script.pxi

+12-15
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ from godot.builtins cimport Array, Dictionary
4242
import inspect
4343
import traceback
4444

45+
from godot.tags import ExportedField, SignalField
46+
4547

4648
cdef inline godot_pluginscript_script_manifest _build_empty_script_manifest():
4749
cdef godot_pluginscript_script_manifest manifest
@@ -125,23 +127,18 @@ cdef godot_pluginscript_script_manifest _build_script_manifest(object cls):
125127
pyobj_to_godot_string_name(base, &manifest.base)
126128

127129
methods = Array()
128-
# TODO: include inherited in exposed methods ? Expose Godot base class' ones ?
129-
# for methname in vars(cls):
130-
for methname in dir(cls):
131-
meth = getattr(cls, methname)
132-
if not is_method(meth) or meth.__name__.startswith("__") or methname.startswith('__'):
133-
continue
134-
methods.append(_build_method_info(meth, methname))
135-
gdapi10.godot_array_new_copy(&manifest.methods, &methods._gd_data)
136-
137130
signals = Array()
138-
for signal in cls.__signals.values():
139-
signals.append(_build_signal_info(signal))
140-
gdapi10.godot_array_new_copy(&manifest.signals, &signals._gd_data)
141-
142131
properties = Array()
143-
for prop in cls.__exported.values():
144-
properties.append(_build_property_info(prop))
132+
for k, v in cls.__exported.items():
133+
if isinstance(v, ExportedField):
134+
properties.append(_build_property_info(v))
135+
elif isinstance(v, SignalField):
136+
signals.append(_build_signal_info(v))
137+
else:
138+
assert is_method(v)
139+
methods.append(_build_method_info(v, k))
140+
gdapi10.godot_array_new_copy(&manifest.methods, &methods._gd_data)
141+
gdapi10.godot_array_new_copy(&manifest.signals, &signals._gd_data)
145142
gdapi10.godot_array_new_copy(&manifest.properties, &properties._gd_data)
146143

147144
return manifest

pythonscript/godot/tags.pyx

+28-14
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,9 @@ class ExportedField:
209209
)
210210

211211
def __call__(self, decorated):
212+
if self.default is not None:
213+
raise ValueError("export should not define a default attribute when used as a decorator")
214+
212215
# This object is used as a decorator
213216
if not callable(decorated) and not isinstance(decorated, builtins.property):
214217
raise ValueError("@export should decorate function or property.")
@@ -248,15 +251,15 @@ def export(
248251
usage::
249252
@exposed
250253
class CustomObject(godot.bindings.Object):
251-
a = exposed(str) # Expose attribute
252-
b = exposed(int, default=42)
254+
a = export(str) # Expose attribute
255+
b = export(int, default=42)
253256
254-
@exposed(int) # Expose property
257+
@export(int) # Expose property
255258
@property
256259
def c(self):
257260
return 42
258261
259-
@exposed(str) # Expose method
262+
@export(str) # Expose method
260263
def d(self):
261264
return "foo"
262265
"""
@@ -298,20 +301,17 @@ def exposed(cls=None, tool=False):
298301
f" (already got {existing_cls_for_module!r})"
299302
)
300303

301-
# Overwrite parent __init__ to avoid creating a Godot object given
302-
# exported script are always initialized with an existing Godot object
303-
cls.__init__ = lambda self: None
304304
cls.__tool = tool
305305
cls.__exposed_python_class = True
306306
cls.__exported = {}
307-
cls.__signals = {}
308307

309-
# Retrieve parent exported fields
308+
# Retrieve parent exported stuff
310309
for b in cls.__bases__:
311310
cls.__exported.update(getattr(b, "__exported", {}))
312-
cls.__signals.update(getattr(b, "__signals", {}))
313311

314-
# Collect exported fields
312+
init_func_code = "def __init__(self):\n pass\n"
313+
314+
# Collect exported stuff: attributes (marked with @exported), properties, signals, and methods
315315
for k, v in cls.__dict__.items():
316316
if isinstance(v, ExportedField):
317317
cls.__exported[k] = v
@@ -321,11 +321,25 @@ def exposed(cls=None, tool=False):
321321
# in the generated class
322322
setattr(cls, k, v.property)
323323
else:
324-
setattr(cls, k, v.default)
324+
# Otherwise, the value must be initialized as part of __init__
325+
if v.default is None or isinstance(v.default, (int, float, bool)):
326+
init_func_code += f" self.{k} = {repr(v.default)}\n"
327+
else:
328+
init_func_code += f" self.{k} = self.__exported['{k}'].default\n"
325329
elif isinstance(v, SignalField):
326-
v.name = v.name if v.name else k
327-
cls.__signals[v.name] = v
330+
v.name = v.name or k
331+
cls.__exported[v.name] = v
328332
setattr(cls, k, v)
333+
elif callable(v):
334+
cls.__exported[k] = v
335+
336+
# Overwrite parent __init__ to avoid creating a Godot object given
337+
# exported script are always initialized with an existing Godot object
338+
# On top of that, we must initialize the attributes defined in the class
339+
# and it parents
340+
g = {}
341+
exec(init_func_code, g)
342+
cls.__init__ = g['__init__']
329343

330344
set_exposed_class(cls)
331345
return cls

0 commit comments

Comments
 (0)