Skip to content

Commit a987c44

Browse files
committed
Move classes.pyx/pxd code into builtins.pyx/pxd (avoid recursive cimport)
1 parent 6a54a0f commit a987c44

File tree

14 files changed

+166
-123
lines changed

14 files changed

+166
-123
lines changed

scripts/generate_tmpl.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
"builtins.pxd": (False, GODOT_DIR),
2222
"builtins.pyx": (False, GODOT_DIR),
2323
"classes.pyi": (True, GODOT_DIR),
24+
"singletons.pyi": (True, GODOT_DIR),
2425
"_classes_api.py": (True, GODOT_DIR),
2526
"utils.pyx": (False, GODOT_DIR),
2627
"utils.pyi": (False, GODOT_DIR),

src/godot/_lang.pyx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ from .hazmat.gdextension_interface cimport *
55
from .hazmat.gdapi cimport *
66
from .hazmat cimport gdptrs
77
from .builtins cimport *
8-
from .classes cimport _load_class, _load_singleton, _cleanup_loaded_classes_and_singletons, BaseGDObject
98

109
#
1110
# Extensions definition

src/godot/_lang_script_language.pxi

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from .classes cimport ScriptLanguageExtensionProfilingInfo
1+
from .hazmat.gdtypes cimport ScriptLanguageExtensionProfilingInfo
22

33

44
debug_spy = True

src/godot/builtins.pxd.j2

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,64 @@ cimport cython
44
from cpython.mem cimport PyMem_Malloc, PyMem_Free
55

66
# Forward declarations
7+
cdef class BaseGDObject
78
{% for builtin in api.builtins %}
89
cdef class {{ builtin.cy_type }}
910
{% endfor %}
1011

1112
from .hazmat cimport gdapi, gdptrs, gdextension_interface
1213
from .hazmat.gdtypes cimport *
13-
from .classes cimport BaseGDObject
14+
15+
16+
##############################################################################
17+
# Godot class dynamic generation & instance operations #
18+
##############################################################################
19+
20+
21+
cpdef object _load_class(StringName name)
22+
cpdef BaseGDObject _load_singleton(str name)
23+
cdef void _cleanup_loaded_classes_and_singletons()
24+
cdef object _object_call(gd_object_t obj, StringName meth, list args)
25+
26+
27+
cdef class BaseGDObject:
28+
cdef gd_object_t _gd_ptr
29+
30+
@staticmethod
31+
cdef inline object steal_cast_from_variant(const gd_variant_t *gdvar):
32+
"""
33+
Return `None` if the variant is not an object (and in this case the caller
34+
is responsible to delete `gdvar`).
35+
Don't increase the refcount if the variant is a refcounted object (hence the
36+
"steal" in the name).
37+
"""
38+
cdef gd_object_t obj = gdapi.gd_object_steal_from_variant(gdvar)
39+
if obj == NULL:
40+
return None
41+
return BaseGDObject.steal_cast_from_object(obj)
42+
43+
@staticmethod
44+
cdef inline object steal_cast_from_object(gd_object_t obj):
45+
"""
46+
Return `None` if object is NULL.
47+
Don't increase the refcount if the variant is a refcounted object (hence the
48+
"steal" in the name).
49+
"""
50+
if obj == NULL:
51+
return None
52+
# TODO: currently `Object.get_class` returns a `String` instead of a `StringName`
53+
# which is kind of wasteful...
54+
cdef StringName class_name = StringName(_object_call(obj, StringName("get_class"), []))
55+
cdef object klass = _load_class(class_name)
56+
return klass._steal_from_ptr(<size_t>obj)
1457

1558

1659
{{ render_all_conversions() }}
60+
61+
62+
##############################################################################
63+
# Builtins wrappers #
64+
##############################################################################
1765
{% for builtin in api.builtins %}
1866

1967

src/godot/builtins.pyi.j2

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,13 @@ class {{ builtin.cy_type }}:
8686
def clone(self) -> {{ builtin.py_type }}: ...
8787
{% if builtin.members %}
8888

89+
# Operators
90+
{# TODO: include other operators! #}
91+
{# Note `Color` has a `hash` method, but it excluded since it is not an immutable type #}
92+
{% if builtin.original_name in ("String", "StringName", "NodePath", "Callable") %}
93+
def __hash__(self): ...
94+
{% endif %}
95+
8996
# Members
9097
{% endif %}
9198
{% for m in builtin.members %}

src/godot/builtins.pyx.j2

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
{%- from 'builtins_pyx/class.pyx.j2' import render_class with context -%}
22
{%- from 'builtins_pyx/conversion.pyx.j2' import render_all_conversions with context -%}
33
cimport cython
4+
from cpython.ref cimport Py_INCREF, Py_DECREF, PyObject
45
from cpython.mem cimport PyMem_Malloc, PyMem_Free
56
from libc.math cimport INFINITY as inf # Needed by some constants
67
from libc.string cimport memset # Needed to create zeroized builtins used in `__bool__`
78

89
# Forward declarations
10+
cdef class BaseGDObject
911
{% for builtin in api.builtins %}
1012
cdef class {{ builtin.cy_type }}
1113
{% endfor %}
@@ -48,12 +50,30 @@ from warnings import warn
4850
cdef object _SENTINEL = object()
4951

5052

53+
##############################################################################
54+
# Godot class dynamic generation & instance operations #
55+
##############################################################################
56+
57+
58+
include "builtins_classes.pxi"
59+
60+
5161
{{ render_all_conversions() }}
62+
63+
64+
##############################################################################
65+
# Builtins wrappers #
66+
##############################################################################
5267
{% for spec in api.builtins %}
5368

5469

5570
{{ render_class(spec) }}
5671
{% endfor %}
72+
73+
74+
##############################################################################
75+
# Global enums #
76+
##############################################################################
5777
{% for spec in api.global_enums %}
5878

5979

Lines changed: 39 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,18 @@
1-
from cpython.ref cimport Py_INCREF, Py_DECREF, PyObject
2-
from .hazmat cimport gdapi, gdptrs, gdextension_interface
3-
from .hazmat.gdtypes cimport *
4-
from .builtins cimport *
1+
# This file is included by `builtins.pxd.j2`
52

63
from enum import IntEnum
74
import inspect
85
from types import UnionType
96
import dataclasses
107

118

12-
#####################################################################
13-
# Godot classes exposed to Python #
14-
#####################################################################
9+
##############################################################################
10+
# BaseGDObject #
11+
##############################################################################
1512

1613

17-
def __getattr__(name: str):
18-
try:
19-
return _load_class(name)
20-
except RuntimeError:
21-
raise AttributeError
14+
# BaseGDObject is the parent class of all Godot classes
15+
# Note it is defined here instead of in `classes.pyx` to avoid recursive import
2216

2317

2418
cdef class BaseGDObject:
@@ -91,6 +85,11 @@ cdef class BaseGDObject:
9185
return wrapper
9286

9387

88+
#####################################################################
89+
# Godot classes exposed to Python #
90+
#####################################################################
91+
92+
9493
cdef object _loaded_singletons = {}
9594
cdef object _loaded_classes = {}
9695

@@ -106,14 +105,13 @@ cpdef BaseGDObject _load_singleton(str name):
106105
except KeyError:
107106
pass
108107

109-
cdef object cls = _load_class(name)
110-
cdef gd_string_name_t gdname = gdapi.gd_string_name_from_unchecked_pystr(name)
111-
cdef gd_object_t gdobj = gdptrs.gdptr_global_get_singleton(&gdname)
112-
gdapi.gd_string_name_del(&gdname)
108+
cdef StringName gd_name = StringName(name)
109+
cdef gd_object_t gdobj = gdptrs.gdptr_global_get_singleton(&gd_name._gd_data)
113110

114111
if gdobj == NULL:
115112
raise RuntimeError(f"Singleton `{name}` doesn't exist in Godot !")
116113

114+
cdef object cls = _load_class(gd_name)
117115
cdef BaseGDObject singleton = <BaseGDObject>cls._steal_from_ptr(<size_t>gdobj)
118116

119117
_loaded_singletons[name] = singleton
@@ -132,29 +130,24 @@ cdef inline object _meth_call(BaseGDObject obj, object name, object args):
132130
return _object_call(obj._gd_ptr, StringName("call"), [name, *args])
133131

134132

135-
cdef object _load_class(str name):
136-
try:
137-
return _loaded_classes[name]
138-
except KeyError:
139-
pass
133+
cdef inline object _build_class_from_spec(str name, StringName gd_name):
134+
# TODO: ClassDB won't be needed once method uses ptrcall
135+
# Load our good friend ClassDB
136+
cdef StringName gdname_classdb = StringName("ClassDB")
137+
cdef gd_object_t classdb = gdptrs.gdptr_global_get_singleton(&gdname_classdb._gd_data)
140138

141139
from godot import _classes_api
140+
cdef list spec
142141
try:
143142
spec = getattr(_classes_api, name)
144143
except AttributeError:
145144
raise RuntimeError(f"Class `{name}` doesn't exist in Godot !")
146145

147-
# TODO: ClassDB won't be needed once method uses ptrcall
148-
# Load our good friend ClassDB
149-
cdef StringName gdname_classdb = StringName("ClassDB")
150-
cdef gd_object_t classdb = gdptrs.gdptr_global_get_singleton(&gdname_classdb._gd_data)
151-
152-
cdef StringName gd_name = StringName(name)
153146
parent = spec[0]
154147
is_refcounted = spec[1]
155148
items_spec = iter(spec[2:])
156149
if parent:
157-
parent_cls = _load_class(parent)
150+
parent_cls = _load_class(StringName(parent))
158151
bases = (parent_cls, )
159152
else:
160153
bases = (BaseGDObject, )
@@ -163,20 +156,23 @@ cdef object _load_class(str name):
163156

164157
if not is_refcounted and name == "RefCounted":
165158

159+
def _gen():
160+
cdef StringName gdstr_unreference = StringName("unreference")
161+
166162
def _del(self):
167163
print(f'[DEBUG] {type(self).__name__}.__del__()', flush=True)
168164
cdef BaseGDObject obj = <BaseGDObject>self
169-
if _object_call(obj._gd_ptr, StringName("unreference"), []):
165+
if _object_call(obj._gd_ptr, gdstr_unreference, []):
170166
gdptrs.gdptr_object_destroy(obj._gd_ptr)
171167
obj._gd_ptr = NULL
172168

173-
attrs["__del__"] = _del
174-
175169
def _free(self):
176170
print(f'[DEBUG] {type(self).__name__}.free()', flush=True)
177171
raise RuntimeError("RefCounted Godot object, cannot be freed")
178172

179-
attrs["free"] = _free
173+
return _del, _free
174+
175+
attrs["__del__"], attrs["free"] = _gen()
180176

181177
while True:
182178
try:
@@ -293,9 +289,18 @@ cdef object _load_class(str name):
293289
# `Object` defines a `free`, but it doesn't work properly (instead we rely on `BaseGDObject.free`)
294290
attrs.pop("free", None)
295291

296-
cdef object klass = type(name, bases, attrs)
292+
return type(name, bases, attrs)
293+
294+
295+
cpdef object _load_class(StringName gd_name):
296+
try:
297+
return _loaded_classes[gd_name]
298+
except KeyError:
299+
pass
300+
301+
cdef object klass = _build_class_from_spec(str(gd_name), gd_name)
297302

298-
_loaded_classes[name] = klass
303+
_loaded_classes[gd_name] = klass
299304
return klass
300305

301306

src/godot/builtins_pyx/operator.pyx.j2

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,15 @@ def __repr__(self):
3030
gdapi.gd_string_del(&gdstr)
3131
{% endif %}
3232

33+
{# Note `Color` has a `hash` method, but it excluded since it is not an immutable type #}
34+
{% if builtin.original_name in ("String", "StringName", "NodePath", "Callable") %}
35+
def __hash__(self):
36+
cdef int64_t ret = gdapi.gd_{{ builtin.snake_name }}_meth_hash(
37+
&self._gd_data,
38+
)
39+
return ret
40+
{% endif %}
41+
3342
def __eq__(self, object other):
3443
cdef {{ builtin.c_type }}* other_gd_data
3544
try:

src/godot/classes.pxd

Lines changed: 0 additions & 40 deletions
This file was deleted.

src/godot/classes.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
from .builtins import _load_class, StringName
2+
3+
4+
def __getattr__(name: str) -> type:
5+
try:
6+
return _load_class(StringName(name))
7+
except RuntimeError:
8+
raise AttributeError

0 commit comments

Comments
 (0)