• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

stevearc / godot_parser / #79

24 Feb 2026 02:14AM UTC coverage: 95.168% (-0.5%) from 95.652%
#79

Pull #22

github

web-flow
Merge 1d95cef8d into 4aec5d3c2
Pull Request #22: Multi godot version support

345 of 374 branches covered (92.25%)

Branch coverage included in aggregate %.

546 of 575 new or added lines in 10 files covered. (94.96%)

3 existing lines in 1 file now uncovered.

1211 of 1261 relevant lines covered (96.03%)

0.96 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

93.84
/godot_parser/objects.py
1
"""Wrappers for Godot's non-primitive object types"""
2

3
import base64
1✔
4
import json
1✔
5
import re
1✔
6
from functools import partial
1✔
7
from math import floor
1✔
8
from typing import Any, Iterable, List, Optional, Type, TypeVar, Union
1✔
9

10
from .output import Outputable, OutputFormat
1✔
11
from .util import Identifiable, stringify_object
1✔
12

13
__all__ = [
1✔
14
    "GDObject",
15
    "Vector2",
16
    "Vector3",
17
    "Color",
18
    "NodePath",
19
    "ResourceReference",
20
    "ExtResource",
21
    "SubResource",
22
    "StringName",
23
    "TypedArray",
24
    "TypedDictionary",
25
    "PackedByteArray",
26
    "PackedVector4Array",
27
    "GDIterable",
28
]
29

30
GD_OBJECT_REGISTRY = {}
1✔
31

32

33
class GDObjectMeta(type):
1✔
34
    """
35
    This is me trying to be too clever for my own good
36

37
    Odds are high that it'll cause some weird hard-to-debug issues at some point, but
38
    isn't it neeeeeat? -_-
39
    """
40

41
    def __new__(cls, name, bases, dct):
1✔
42
        x = super().__new__(cls, name, bases, dct)
1✔
43
        GD_OBJECT_REGISTRY[name] = x
1✔
44
        return x
1✔
45

46

47
GDObjectType = TypeVar("GDObjectType", bound="GDObject")
1✔
48

49

50
class GDObject(Outputable, metaclass=GDObjectMeta):
1✔
51
    """
52
    Base class for all GD Object types
53

54
    Can be used to represent any GD type. For example::
55

56
        GDObject('Vector2', 1, 2) == Vector2(1, 2)
57
    """
58

59
    def __init__(self, name, *args) -> None:
1✔
60
        self.name = name
1✔
61
        self.args: List[Any] = list(args)
1✔
62

63
    def __contains__(self, idx: int) -> bool:
1✔
NEW
64
        return idx in self.args
×
65

66
    def __getitem__(self, idx: int) -> float:
1✔
67
        return self.args[idx]
1✔
68

69
    def __setitem__(self, idx: int, value: float) -> None:
1✔
70
        self.args[idx] = value
1✔
71

72
    def __delitem__(self, idx: int) -> None:
1✔
NEW
73
        del self.args[idx]
×
74

75
    @classmethod
1✔
76
    def from_parser(cls: Type[GDObjectType], parse_result) -> GDObjectType:
1✔
77
        name = parse_result[0]
1✔
78
        factory = GD_OBJECT_REGISTRY.get(name, partial(GDObject, name))
1✔
79
        return factory(*parse_result[1:])
1✔
80

81
    __packed_array_re = re.compile(r"^(Packed|Pool)(?P<InnerType>[A-Z]\w+)Array$")
1✔
82

83
    def _output_to_string(self, output_format: OutputFormat) -> str:
1✔
84
        name = self.name
1✔
85

86
        match = self.__packed_array_re.match(name)
1✔
87
        if match:
1✔
88
            name = output_format.packed_array_format % match.group("InnerType")
1✔
89

90
        return name + output_format.surround_parentheses(
1✔
91
            ", ".join([stringify_object(v, output_format) for v in self.args])
92
        )
93

94
    def __repr__(self) -> str:
1✔
95
        return self.__str__()
1✔
96

97
    def __eq__(self, other) -> bool:
1✔
98
        if not isinstance(other, GDObject):
1✔
99
            return False
1✔
100
        return self.name == other.name and self.args == other.args
1✔
101

102
    def __ne__(self, other) -> bool:
1✔
103
        return not self.__eq__(other)
1✔
104

105
    def __hash__(self):
1✔
106
        return hash(frozenset((self.name, *self.args)))
1✔
107

108

109
class Vector2(GDObject):
1✔
110
    def __init__(self, x: float, y: float) -> None:
1✔
111
        super().__init__("Vector2", x, y)
1✔
112

113
    @property
1✔
114
    def x(self) -> float:
1✔
115
        """Getter for x"""
116
        return self.args[0]
1✔
117

118
    @x.setter
1✔
119
    def x(self, x: float) -> None:
1✔
120
        """Setter for x"""
121
        self.args[0] = x
1✔
122

123
    @property
1✔
124
    def y(self) -> float:
1✔
125
        """Getter for y"""
126
        return self.args[1]
1✔
127

128
    @y.setter
1✔
129
    def y(self, y: float) -> None:
1✔
130
        """Setter for y"""
131
        self.args[1] = y
1✔
132

133

134
class Vector3(GDObject):
1✔
135
    def __init__(self, x: float, y: float, z: float) -> None:
1✔
136
        super().__init__("Vector3", x, y, z)
1✔
137

138
    @property
1✔
139
    def x(self) -> float:
1✔
140
        """Getter for x"""
141
        return self.args[0]
1✔
142

143
    @x.setter
1✔
144
    def x(self, x: float) -> None:
1✔
145
        """Setter for x"""
146
        self.args[0] = x
1✔
147

148
    @property
1✔
149
    def y(self) -> float:
1✔
150
        """Getter for y"""
151
        return self.args[1]
1✔
152

153
    @y.setter
1✔
154
    def y(self, y: float) -> None:
1✔
155
        """Setter for y"""
156
        self.args[1] = y
1✔
157

158
    @property
1✔
159
    def z(self) -> float:
1✔
160
        """Getter for z"""
161
        return self.args[2]
1✔
162

163
    @z.setter
1✔
164
    def z(self, z: float) -> None:
1✔
165
        """Setter for z"""
166
        self.args[2] = z
1✔
167

168

169
class Vector4(GDObject):
1✔
170
    def __init__(self, x: float, y: float, z: float, w: float) -> None:
1✔
171
        super().__init__("Vector4", x, y, z, w)
1✔
172

173
    @property
1✔
174
    def x(self) -> float:
1✔
175
        """Getter for x"""
176
        return self.args[0]
1✔
177

178
    @x.setter
1✔
179
    def x(self, x: float) -> None:
1✔
180
        """Setter for x"""
UNCOV
181
        self.args[0] = x
×
182

183
    @property
1✔
184
    def y(self) -> float:
1✔
185
        """Getter for y"""
186
        return self.args[1]
1✔
187

188
    @y.setter
1✔
189
    def y(self, y: float) -> None:
1✔
190
        """Setter for y"""
UNCOV
191
        self.args[1] = y
×
192

193
    @property
1✔
194
    def z(self) -> float:
1✔
195
        """Getter for z"""
196
        return self.args[2]
1✔
197

198
    @z.setter
1✔
199
    def z(self, z: float) -> None:
1✔
200
        """Setter for z"""
UNCOV
201
        self.args[2] = z
×
202

203
    @property
1✔
204
    def w(self) -> float:
1✔
205
        """Getter for w"""
206
        return self.args[3]
1✔
207

208
    @w.setter
1✔
209
    def w(self, w: float) -> None:
1✔
210
        """Setter for w"""
NEW
211
        self.args[3] = w
×
212

213

214
class Color(GDObject):
1✔
215
    def __init__(self, r: float, g: float, b: float, a: float) -> None:
1✔
216
        assert 0 <= r <= 1
1✔
217
        assert 0 <= g <= 1
1✔
218
        assert 0 <= b <= 1
1✔
219
        assert 0 <= a <= 1
1✔
220
        super().__init__("Color", r, g, b, a)
1✔
221

222
    @property
1✔
223
    def r(self) -> float:
1✔
224
        """Getter for r"""
225
        return self.args[0]
1✔
226

227
    @r.setter
1✔
228
    def r(self, r: float) -> None:
1✔
229
        """Setter for r"""
230
        self.args[0] = r
1✔
231

232
    @property
1✔
233
    def g(self) -> float:
1✔
234
        """Getter for g"""
235
        return self.args[1]
1✔
236

237
    @g.setter
1✔
238
    def g(self, g: float) -> None:
1✔
239
        """Setter for g"""
240
        self.args[1] = g
1✔
241

242
    @property
1✔
243
    def b(self) -> float:
1✔
244
        """Getter for b"""
245
        return self.args[2]
1✔
246

247
    @b.setter
1✔
248
    def b(self, b: float) -> None:
1✔
249
        """Setter for b"""
250
        self.args[2] = b
1✔
251

252
    @property
1✔
253
    def a(self) -> float:
1✔
254
        """Getter for a"""
255
        return self.args[3]
1✔
256

257
    @a.setter
1✔
258
    def a(self, a: float) -> None:
1✔
259
        """Setter for a"""
260
        self.args[3] = a
1✔
261

262

263
class PackedVector4Array(GDObject):
1✔
264
    def __init__(self, *args) -> None:
1✔
265
        super().__init__("PackedVector4Array", *args)
1✔
266

267
    @classmethod
1✔
268
    def FromList(cls, list_: List[Vector4]) -> "PackedVector4Array":
1✔
269
        return cls(*sum([[v.x, v.y, v.z, v.w] for v in list_], []))
1✔
270

271
    def get_vector4(self, idx: int) -> Vector4:
1✔
272
        return Vector4(
1✔
273
            self.args[idx * 4 + 0],
274
            self.args[idx * 4 + 1],
275
            self.args[idx * 4 + 2],
276
            self.args[idx * 4 + 3],
277
        )
278

279
    def set_vector4(self, idx: int, value: Vector4) -> None:
1✔
NEW
280
        self.args[idx * 4 + 0] = value.x
×
NEW
281
        self.args[idx * 4 + 1] = value.y
×
NEW
282
        self.args[idx * 4 + 2] = value.z
×
NEW
283
        self.args[idx * 4 + 3] = value.w
×
284

285
    def remove_vector4_at(self, idx: int) -> None:
1✔
286
        del self.args[idx * 4]
1✔
287
        del self.args[idx * 4]
1✔
288
        del self.args[idx * 4]
1✔
289
        del self.args[idx * 4]
1✔
290

291
    def _output_to_string(self, output_format: OutputFormat) -> str:
1✔
292
        if output_format.packed_vector4_array_support:
1✔
293
            return super()._output_to_string(output_format)
1✔
294
        else:
295
            return TypedArray(
1✔
296
                "Vector4",
297
                [self.get_vector4(i) for i in range(floor(len(self.args) / 4))],
298
            ).output_to_string(output_format)
299

300

301
class PackedByteArray(GDObject):
1✔
302
    def __init__(self, *args) -> None:
1✔
303
        super().__init__("PackedByteArray", *args)
1✔
304

305
    @classmethod
1✔
306
    def FromBytes(cls, bytes_: bytes) -> "PackedByteArray":
1✔
307
        return cls(*list(bytes_))
1✔
308

309
    def _stored_as_base64(self) -> bool:
1✔
310
        return len(self.args) == 1 and isinstance(self.args[0], str)
1✔
311

312
    @property
1✔
313
    def bytes_(self) -> bytes:
1✔
314
        if self._stored_as_base64():
1✔
315
            return base64.b64decode(self.args[0])
1✔
316
        return bytes(self.args)
1✔
317

318
    @bytes_.setter
1✔
319
    def bytes_(self, bytes_: bytes) -> None:
1✔
320
        self.args = list(bytes_)
1✔
321

322
    def _output_to_string(self, output_format: OutputFormat) -> str:
1✔
323
        if output_format.packed_byte_array_base64_support:
1✔
324
            if not self._stored_as_base64():
1✔
325
                self.args = [base64.b64encode(self.bytes_).decode("utf-8")]
1✔
326
        elif self._stored_as_base64():
1✔
327
            self.bytes_ = self.bytes_
1✔
328
        return super()._output_to_string(output_format)
1✔
329

330

331
class NodePath(GDObject):
1✔
332
    def __init__(self, path: str) -> None:
1✔
333
        super().__init__("NodePath", path)
1✔
334

335
    @property
1✔
336
    def path(self) -> str:
1✔
337
        """Getter for path"""
338
        return self.args[0]
1✔
339

340
    @path.setter
1✔
341
    def path(self, path: str) -> None:
1✔
342
        """Setter for path"""
343
        self.args[0] = path
1✔
344

345
    def _output_to_string(self, output_format: OutputFormat) -> str:
1✔
346
        original_punctuation_spaces = output_format.punctuation_spaces
1✔
347
        output_format.punctuation_spaces = False
1✔
348
        ret = super()._output_to_string(output_format)
1✔
349
        output_format.punctuation_spaces = original_punctuation_spaces
1✔
350
        return ret
1✔
351

352

353
class ResourceReference(GDObject):
1✔
354
    def __init__(self, name: str, resource: Union[int, str, Identifiable]):
1✔
355
        self.resource = resource
1✔
356
        if isinstance(resource, Identifiable):
1✔
357
            super().__init__(name)
1✔
358
        else:
359
            super().__init__(name, resource)
1✔
360

361
    @property
1✔
362
    def id(self) -> Optional[Union[int, str]]:
1✔
363
        """Getter for id"""
364
        if isinstance(self.resource, Identifiable):
1✔
365
            return self.resource.get_id()
1✔
366
        else:
367
            return self.resource
1✔
368

369
    @id.setter
1✔
370
    def id(self, id: Union[int, str]) -> None:
1✔
371
        """Setter for id"""
372
        self.resource = id
1✔
373
        self.args = [id]
1✔
374

375
    @classmethod
1✔
376
    def get_id_key(cls, index: Optional[int] = None) -> str:
1✔
377
        return "Resource"
1✔
378

379
    def _output_to_string(self, output_format: OutputFormat) -> str:
1✔
380
        if isinstance(self.resource, Identifiable):
1✔
381
            id = self.resource.get_id()
1✔
382
            if id is not None:
1!
383
                self.id = id
1✔
384
        return super()._output_to_string(output_format)
1✔
385

386

387
class ExtResource(ResourceReference):
1✔
388
    def __init__(self, resource: Union[int, str, Identifiable]) -> None:
1✔
389
        super().__init__("ExtResource", resource)
1✔
390

391
    @classmethod
1✔
392
    def get_id_key(cls, index: Optional[int] = None) -> str:
1✔
393
        return str(index)
1✔
394

395

396
class SubResource(ResourceReference):
1✔
397
    def __init__(self, resource: Union[int, str, Identifiable]) -> None:
1✔
398
        super().__init__("SubResource", resource)
1✔
399

400

401
class GDIterable:
1✔
402
    def _iter_objects(self) -> Iterable[Any]:
1✔
NEW
403
        return iter([])
×
404

405

406
class TypedArray(GDIterable, Outputable):
1✔
407
    def __init__(self, type, list_) -> None:
1✔
408
        self.name = "Array"
1✔
409
        self.type = type
1✔
410
        self.list_: list = list_
1✔
411

412
    @classmethod
1✔
413
    def WithCustomName(cls: Type["TypedArray"], name, type, list_) -> "TypedArray":
1✔
414
        custom_array = TypedArray(type, list_)
1✔
415
        custom_array.name = name
1✔
416
        return custom_array
1✔
417

418
    @classmethod
1✔
419
    def from_parser(cls: Type["TypedArray"], parse_result) -> "TypedArray":
1✔
420
        return TypedArray.WithCustomName(*parse_result)
1✔
421

422
    def _output_to_string(self, output_format: OutputFormat) -> str:
1✔
423
        if output_format.typed_array_support:
1✔
424
            return (
1✔
425
                self.name
426
                + output_format.surround_brackets(
427
                    self.type.output_to_string(output_format)
428
                    if isinstance(self.type, Outputable)
429
                    else self.type
430
                )
431
                + output_format.surround_parentheses(
432
                    stringify_object(self.list_, output_format)
433
                )
434
            )
435
        else:
436
            return stringify_object(self.list_, output_format)
1✔
437

438
    def __repr__(self) -> str:
1✔
439
        return self.__str__()
1✔
440

441
    def __eq__(self, other) -> bool:
1✔
442
        if not isinstance(other, TypedArray):
1!
443
            return False
×
444
        return (
1✔
445
            self.name == other.name
446
            and self.type == other.type
447
            and self.list_ == other.list_
448
        )
449

450
    def __ne__(self, other) -> bool:
1✔
451
        return not self.__eq__(other)
×
452

453
    def __hash__(self):
1✔
454
        return hash(frozenset((self.name, self.type, self.list_)))
×
455

456
    def _iter_objects(self) -> Iterable[Any]:
1✔
457
        yield self.type
1✔
458
        yield from self.list_
1✔
459

460

461
class TypedDictionary(GDIterable, Outputable):
1✔
462
    def __init__(self, key_type, value_type, dict_) -> None:
1✔
463
        self.name = "Dictionary"
1✔
464
        self.key_type = key_type
1✔
465
        self.value_type = value_type
1✔
466
        self.dict_: dict = dict_
1✔
467

468
    @classmethod
1✔
469
    def WithCustomName(
1✔
470
        cls: Type["TypedDictionary"], name, key_type, value_type, dict_
471
    ) -> "TypedDictionary":
472
        custom_dict = TypedDictionary(key_type, value_type, dict_)
1✔
473
        custom_dict.name = name
1✔
474
        return custom_dict
1✔
475

476
    @classmethod
1✔
477
    def from_parser(cls: Type["TypedDictionary"], parse_result) -> "TypedDictionary":
1✔
478
        return TypedDictionary.WithCustomName(*parse_result)
1✔
479

480
    def _output_to_string(self, output_format: OutputFormat) -> str:
1✔
481
        if output_format.typed_dictionary_support:
1✔
482
            return (
1✔
483
                self.name
484
                + output_format.surround_brackets(
485
                    "%s, %s"
486
                    % (
487
                        (
488
                            self.key_type.output_to_string(output_format)
489
                            if isinstance(self.key_type, Outputable)
490
                            else self.key_type
491
                        ),
492
                        (
493
                            self.value_type.output_to_string(output_format)
494
                            if isinstance(self.value_type, Outputable)
495
                            else self.value_type
496
                        ),
497
                    )
498
                )
499
                + output_format.surround_parentheses(
500
                    stringify_object(self.dict_, output_format)
501
                )
502
            )
503
        else:
504
            return stringify_object(self.dict_, output_format)
1✔
505

506
    def __repr__(self) -> str:
1✔
507
        return self.__str__()
1✔
508

509
    def __eq__(self, other) -> bool:
1✔
510
        if not isinstance(other, TypedDictionary):
1!
511
            return False
×
512
        return (
1✔
513
            self.name == other.name
514
            and self.key_type == other.key_type
515
            and self.value_type == other.value_type
516
            and self.dict_ == other.dict_
517
        )
518

519
    def __ne__(self, other) -> bool:
1✔
520
        return not self.__eq__(other)
×
521

522
    def __hash__(self):
1✔
523
        return hash(frozenset((self.name, self.key_type, self.value_type, self.dict_)))
×
524

525
    def _iter_objects(self) -> Iterable[Any]:
1✔
526
        yield self.key_type
1✔
527
        yield self.value_type
1✔
528
        yield from self.dict_.keys()
1✔
529
        yield from self.dict_.values()
1✔
530

531

532
class StringName(Outputable):
1✔
533
    def __init__(self, str) -> None:
1✔
534
        self.str = str
1✔
535

536
    @classmethod
1✔
537
    def from_parser(cls: Type["StringName"], parse_result) -> "StringName":
1✔
538
        return StringName(parse_result[0])
1✔
539

540
    def _output_to_string(self, output_format: OutputFormat) -> str:
1✔
541
        if output_format.string_name_support:
1✔
542
            return "&" + json.dumps(self.str, ensure_ascii=False).replace("'", "\\'")
1✔
543
        else:
544
            return stringify_object(self.str, output_format)
1✔
545

546
    def __repr__(self) -> str:
1✔
547
        return self.__str__()
1✔
548

549
    def __eq__(self, other) -> bool:
1✔
550
        if not isinstance(other, StringName):
1!
551
            return False
×
552
        return self.str == other.str
1✔
553

554
    def __ne__(self, other) -> bool:
1✔
555
        return not self.__eq__(other)
1✔
556

557
    def __hash__(self):
1✔
558
        return hash(self.str)
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc