From 19f18ba1968407e47489732a05163f9d84919326 Mon Sep 17 00:00:00 2001 From: DougVanny Date: Tue, 17 Feb 2026 15:37:02 -0300 Subject: [PATCH 01/21] StringName support --- godot_parser/objects.py | 25 +++++++++++++++++++++++++ godot_parser/values.py | 9 +++++++-- tests/test_parser.py | 17 ++++++++++++++++- 3 files changed, 48 insertions(+), 3 deletions(-) diff --git a/godot_parser/objects.py b/godot_parser/objects.py index 42d5004..a2b30c8 100644 --- a/godot_parser/objects.py +++ b/godot_parser/objects.py @@ -13,6 +13,7 @@ "NodePath", "ExtResource", "SubResource", + "StringName", ] GD_OBJECT_REGISTRY = {} @@ -245,3 +246,27 @@ def id(self) -> int: def id(self, id: int) -> None: """Setter for id""" self.args[0] = id + +class StringName(): + def __init__(self, str) -> None: + self.str = str + + @classmethod + def from_parser(cls: Type[StringName], parse_result) -> StringName: + return StringName(parse_result[0]) + + def __str__(self) -> str: + return "&\"%s\"" % ( + self.str, + ) + + def __repr__(self) -> str: + return self.__str__() + + def __eq__(self, other) -> bool: + if not isinstance(other, StringName): + return False + return self.str == other.str + + def __ne__(self, other) -> bool: + return not self.__eq__(other) \ No newline at end of file diff --git a/godot_parser/values.py b/godot_parser/values.py index 1b43031..fe6fa77 100644 --- a/godot_parser/values.py +++ b/godot_parser/values.py @@ -14,7 +14,7 @@ common, ) -from .objects import GDObject +from .objects import GDObject, StringName boolean = ( (Keyword("true") | Keyword("false")) @@ -24,9 +24,14 @@ null = Keyword("null").set_parse_action(lambda _: [None]) +_string = QuotedString('"', escChar="\\", multiline=True).set_name("string") + +_string_name = ( + Suppress('&') + _string +).set_name("string_name").set_parse_action(StringName.from_parser) primitive = ( - null | QuotedString('"', escChar="\\", multiline=True) | boolean | common.number + null | _string | _string_name | boolean | common.number ) value = Forward() diff --git a/tests/test_parser.py b/tests/test_parser.py index 975166f..d526495 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -3,7 +3,7 @@ from pyparsing import ParseException -from godot_parser import GDFile, GDObject, GDSection, GDSectionHeader, Vector2, parse +from godot_parser import GDFile, GDObject, GDSection, GDSectionHeader, Vector2, StringName, parse HERE = os.path.dirname(__file__) @@ -110,6 +110,21 @@ ) ), ), + ( + """[sub_resource type="CustomType" id=1] + string_value = "String" + string_name_value = &"StringName" + """ , +GDFile( + GDSection( + GDSectionHeader("sub_resource", type="CustomType", id=1), + **{ + "string_value": "String", + "string_name_value": StringName("StringName"), + } + ) + ), + ), ] From f38867e0fd1d0d9eb931f1c7a35069012297bb8a Mon Sep 17 00:00:00 2001 From: DougVanny Date: Tue, 17 Feb 2026 15:38:31 -0300 Subject: [PATCH 02/21] Update .gitignore --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index ea21f34..5616a14 100644 --- a/.gitignore +++ b/.gitignore @@ -129,3 +129,6 @@ dmypy.json .pyre/ .direnv/ + +#PyCharm +.idea \ No newline at end of file From 8c9011cc3dd384ad65d6d7f62e4bff34119e6067 Mon Sep 17 00:00:00 2001 From: DougVanny Date: Tue, 17 Feb 2026 15:47:18 -0300 Subject: [PATCH 03/21] Updated parseAll to parse_all --- godot_parser/files.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/godot_parser/files.py b/godot_parser/files.py index adf5e73..87483f6 100644 --- a/godot_parser/files.py +++ b/godot_parser/files.py @@ -304,7 +304,7 @@ def get_node(self, path: str = ".") -> Optional[GDNodeSection]: @classmethod def parse(cls: Type[GDFileType], contents: str) -> GDFileType: """Parse the contents of a Godot file""" - return cls.from_parser(scene_file.parse_string(contents, parseAll=True)) + return cls.from_parser(scene_file.parse_string(contents, parse_all=True)) @classmethod def load(cls: Type[GDFileType], filepath: str) -> GDFileType: From 66118bb777a151cef54962cf94a9d04f7c3e5ed8 Mon Sep 17 00:00:00 2001 From: DougVanny Date: Tue, 17 Feb 2026 16:35:50 -0300 Subject: [PATCH 04/21] Ignoring whitespaces on test_parse_files and outputting StringName with inner quotes --- godot_parser/objects.py | 4 +--- test_parse_files.py | 7 +++++-- tests/test_parser.py | 4 ++++ 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/godot_parser/objects.py b/godot_parser/objects.py index a2b30c8..d6bfd36 100644 --- a/godot_parser/objects.py +++ b/godot_parser/objects.py @@ -256,9 +256,7 @@ def from_parser(cls: Type[StringName], parse_result) -> StringName: return StringName(parse_result[0]) def __str__(self) -> str: - return "&\"%s\"" % ( - self.str, - ) + return "&" + stringify_object(self.str) def __repr__(self) -> str: return self.__str__() diff --git a/test_parse_files.py b/test_parse_files.py index bccb7b0..55e0988 100755 --- a/test_parse_files.py +++ b/test_parse_files.py @@ -2,10 +2,13 @@ import argparse import os import sys +import re from itertools import zip_longest from godot_parser import load, parse +# Regex to detect all whitespaces not between quotes +line_normalizer_re = re.compile('\s+(?=((\\[\\"]|[^\\"])*"(\\[\\"]|[^\\"])*")*(\\[\\"]|[^\\"])*$)') def _parse_and_test_file(filename: str) -> bool: print("Parsing %s" % filename) @@ -24,8 +27,8 @@ def _parse_and_test_file(filename: str) -> bool: with f.use_tree() as tree: pass - data_lines = [l for l in str(data).split("\n") if l] - content_lines = [l for l in contents.split("\n") if l] + data_lines = [line_normalizer_re.sub("",l) for l in str(data).split("\n") if l] + content_lines = [line_normalizer_re.sub("",l) for l in contents.split("\n") if l] if data_lines != content_lines: print(" Error!") max_len = max([len(l) for l in content_lines]) diff --git a/tests/test_parser.py b/tests/test_parser.py index d526495..caaa312 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -114,6 +114,8 @@ """[sub_resource type="CustomType" id=1] string_value = "String" string_name_value = &"StringName" + string_quote = "\\"String\\"" + string_name_quote = &"\\"StringName\\"" """ , GDFile( GDSection( @@ -121,6 +123,8 @@ **{ "string_value": "String", "string_name_value": StringName("StringName"), + "string_quote": "\"String\"", + "string_name_quote": StringName("\"StringName\""), } ) ), From 8f01daaa5f461c1426c1697a1d7f1392ac9b2612 Mon Sep 17 00:00:00 2001 From: DougVanny Date: Tue, 17 Feb 2026 16:57:10 -0300 Subject: [PATCH 05/21] Removed whitespace from GDObject string representation and added StringName tests to test_objects --- godot_parser/objects.py | 2 +- tests/test_gdfile.py | 2 +- tests/test_objects.py | 26 +++++++++++++++++++------- 3 files changed, 21 insertions(+), 9 deletions(-) diff --git a/godot_parser/objects.py b/godot_parser/objects.py index d6bfd36..6b9b4d5 100644 --- a/godot_parser/objects.py +++ b/godot_parser/objects.py @@ -56,7 +56,7 @@ def from_parser(cls: Type[GDObjectType], parse_result) -> GDObjectType: return factory(*parse_result[1:]) def __str__(self) -> str: - return "%s( %s )" % ( + return "%s(%s)" % ( self.name, ", ".join([stringify_object(v) for v in self.args]), ) diff --git a/tests/test_gdfile.py b/tests/test_gdfile.py index 7706e0f..f2e35dd 100644 --- a/tests/test_gdfile.py +++ b/tests/test_gdfile.py @@ -29,7 +29,7 @@ def test_all_data_types(self): [resource] list = [ 1, 2.0, "string" ] map = { -"key": [ "nested", Vector2( 1, 1 ) ] +"key": [ "nested", Vector2(1, 1) ] } empty = null escaped = "foo(\\"bar\\")" diff --git a/tests/test_objects.py b/tests/test_objects.py index 89598dc..caf840b 100644 --- a/tests/test_objects.py +++ b/tests/test_objects.py @@ -1,6 +1,6 @@ import unittest -from godot_parser import Color, ExtResource, NodePath, SubResource, Vector2, Vector3 +from godot_parser import Color, ExtResource, NodePath, SubResource, Vector2, Vector3, StringName class TestGDObjects(unittest.TestCase): @@ -13,7 +13,7 @@ def test_vector2(self): self.assertEqual(v[1], 2) self.assertEqual(v.x, 1) self.assertEqual(v.y, 2) - self.assertEqual(str(v), "Vector2( 1, 2 )") + self.assertEqual(str(v), "Vector2(1, 2)") v.x = 2 v.y = 3 self.assertEqual(v.x, 2) @@ -32,7 +32,7 @@ def test_vector3(self): self.assertEqual(v.x, 1) self.assertEqual(v.y, 2) self.assertEqual(v.z, 3) - self.assertEqual(str(v), "Vector3( 1, 2, 3 )") + self.assertEqual(str(v), "Vector3(1, 2, 3)") v.x = 2 v.y = 3 v.z = 4 @@ -57,7 +57,7 @@ def test_color(self): self.assertEqual(c.g, 0.2) self.assertEqual(c.b, 0.3) self.assertEqual(c.a, 0.4) - self.assertEqual(str(c), "Color( 0.1, 0.2, 0.3, 0.4 )") + self.assertEqual(str(c), "Color(0.1, 0.2, 0.3, 0.4)") c.r = 0.2 c.g = 0.3 c.b = 0.4 @@ -89,7 +89,7 @@ def test_ext_resource(self): self.assertEqual(r.id, 1) r.id = 2 self.assertEqual(r.id, 2) - self.assertEqual(str(r), "ExtResource( 2 )") + self.assertEqual(str(r), "ExtResource(2)") def test_sub_resource(self): """Test for SubResource""" @@ -97,14 +97,26 @@ def test_sub_resource(self): self.assertEqual(r.id, 1) r.id = 2 self.assertEqual(r.id, 2) - self.assertEqual(str(r), "SubResource( 2 )") + self.assertEqual(str(r), "SubResource(2)") def test_dunder(self): """Test the __magic__ methods on GDObject""" v = Vector2(1, 2) - self.assertEqual(repr(v), "Vector2( 1, 2 )") + self.assertEqual(repr(v), "Vector2(1, 2)") v2 = Vector2(1, 2) self.assertEqual(v, v2) v2.x = 10 self.assertNotEqual(v, v2) self.assertNotEqual(v, (1, 2)) + + def test_string_name(self): + """Test for StringName""" + s = StringName("test") + self.assertEqual(repr(s), "&\"test\"") + s2 = StringName("test") + self.assertEqual(s, s2) + s2.str = "bad" + self.assertNotEqual(s, s2) + + s = StringName("A \"Quoted test\"") + self.assertEqual(repr(s), "&\"A \\\"Quoted test\\\"\"") From 2b7410fa0d5f42f9cfb0d85d53fdf60415399833 Mon Sep 17 00:00:00 2001 From: DougVanny Date: Tue, 17 Feb 2026 17:05:39 -0300 Subject: [PATCH 06/21] Removed regex warning --- test_parse_files.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test_parse_files.py b/test_parse_files.py index 55e0988..0c0d0d5 100755 --- a/test_parse_files.py +++ b/test_parse_files.py @@ -8,7 +8,7 @@ from godot_parser import load, parse # Regex to detect all whitespaces not between quotes -line_normalizer_re = re.compile('\s+(?=((\\[\\"]|[^\\"])*"(\\[\\"]|[^\\"])*")*(\\[\\"]|[^\\"])*$)') +line_normalizer_re = re.compile('\\s+(?=((\\[\\"]|[^\\"])*"(\\[\\"]|[^\\"])*")*(\\[\\"]|[^\\"])*$)') def _parse_and_test_file(filename: str) -> bool: print("Parsing %s" % filename) From 792b3103480ae2d358dc479510b7ddf25cd1424f Mon Sep 17 00:00:00 2001 From: DougVanny Date: Tue, 17 Feb 2026 18:42:38 -0300 Subject: [PATCH 07/21] Typed Dictionary support --- godot_parser/objects.py | 52 ++++++++++++++++++++++++++++++++++++++++- godot_parser/util.py | 2 +- godot_parser/values.py | 23 ++++++++++++++---- tests/test_objects.py | 11 ++++++++- tests/test_parser.py | 27 ++++++++++++++++++++- 5 files changed, 106 insertions(+), 9 deletions(-) diff --git a/godot_parser/objects.py b/godot_parser/objects.py index 6b9b4d5..d05a6bb 100644 --- a/godot_parser/objects.py +++ b/godot_parser/objects.py @@ -14,6 +14,7 @@ "ExtResource", "SubResource", "StringName", + "TypedDictionary", ] GD_OBJECT_REGISTRY = {} @@ -72,6 +73,9 @@ def __eq__(self, other) -> bool: def __ne__(self, other) -> bool: return not self.__eq__(other) + def __hash__(self): + return hash(frozenset((self.name,*self.args))) + class Vector2(GDObject): def __init__(self, x: float, y: float) -> None: @@ -247,6 +251,49 @@ def id(self, id: int) -> None: """Setter for id""" self.args[0] = id + +class TypedDictionary(): + def __init__(self, key_type, value_type, dict_) -> None: + self.name = "Dictionary" + self.key_type = key_type + self.value_type = value_type + self.dict_ = dict_ + + @classmethod + def WithCustomName(cls: Type[TypedDictionary], name, key_type, value_type, dict_) -> TypedDictionary: + custom_dict_ = TypedDictionary(key_type, value_type, dict_) + custom_dict_.name = name + return custom_dict_ + + @classmethod + def from_parser(cls: Type[TypedDictionary], parse_result) -> TypedDictionary: + return TypedDictionary.WithCustomName(*parse_result) + + def __str__(self) -> str: + return "%s[%s, %s](%s)" % ( + self.name, + self.key_type, + self.value_type, + stringify_object(self.dict_) + ) + + def __repr__(self) -> str: + return self.__str__() + + def __eq__(self, other) -> bool: + if not isinstance(other, TypedDictionary): + return False + return self.name == other.name and \ + self.key_type == other.key_type and \ + self.value_type == other.value_type and \ + self.dict_ == other.dict_ + + def __ne__(self, other) -> bool: + return not self.__eq__(other) + + def __hash__(self): + return hash(frozenset((self.name,self.key_type,self.value_type,self.dict_))) + class StringName(): def __init__(self, str) -> None: self.str = str @@ -267,4 +314,7 @@ def __eq__(self, other) -> bool: return self.str == other.str def __ne__(self, other) -> bool: - return not self.__eq__(other) \ No newline at end of file + return not self.__eq__(other) + + def __hash__(self): + return hash(self.str) \ No newline at end of file diff --git a/godot_parser/util.py b/godot_parser/util.py index 42b1433..76e4b56 100644 --- a/godot_parser/util.py +++ b/godot_parser/util.py @@ -17,7 +17,7 @@ def stringify_object(value): return ( "{\n" + ",\n".join( - ['"%s": %s' % (k, stringify_object(v)) for k, v in value.items()] + ['%s: %s' % (stringify_object(k), stringify_object(v)) for k, v in value.items()] ) + "\n}" ) diff --git a/godot_parser/values.py b/godot_parser/values.py index fe6fa77..f8b4b72 100644 --- a/godot_parser/values.py +++ b/godot_parser/values.py @@ -14,7 +14,7 @@ common, ) -from .objects import GDObject, StringName +from .objects import GDObject, StringName, TypedDictionary boolean = ( (Keyword("true") | Keyword("false")) @@ -36,13 +36,15 @@ value = Forward() # Vector2( 1, 2 ) -obj_type = ( +obj_ = ( Word(alphas, alphanums).set_results_name("object_name") + Suppress("(") - + DelimitedList(value) + + Opt(DelimitedList(value)) + Suppress(")") ).set_parse_action(GDObject.from_parser) +obj_type = obj_ | Word(alphas, alphanums) + # [ 1, 2 ] or [ 1, 2, ] list_ = ( Group( @@ -51,7 +53,7 @@ .set_name("list") .set_parse_action(lambda p: p.as_list()) ) -key_val = Group(QuotedString('"', escChar="\\") + Suppress(":") + value) +key_val = Group(value + Suppress(":") + value) # { # "_edit_use_anchors_": false @@ -62,6 +64,17 @@ .set_parse_action(lambda d: {k: v for k, v in d}) ) +typed_dict = ( + Word(alphas, alphanums).set_results_name("object_name") + + ( + Suppress("[") + + obj_type.set_results_name("key_type") + + Suppress(",") + + obj_type.set_results_name("value_type") + + Suppress("]") + ) + Suppress("(") + dict_ + Suppress(")") +).set_parse_action(TypedDictionary.from_parser) + # Exports -value <<= primitive | list_ | dict_ | obj_type +value <<= list_ | dict_ | typed_dict | obj_ | primitive diff --git a/tests/test_objects.py b/tests/test_objects.py index caf840b..aa3d72c 100644 --- a/tests/test_objects.py +++ b/tests/test_objects.py @@ -1,6 +1,6 @@ import unittest -from godot_parser import Color, ExtResource, NodePath, SubResource, Vector2, Vector3, StringName +from godot_parser import Color, ExtResource, NodePath, SubResource, Vector2, Vector3, StringName, GDObject, TypedDictionary class TestGDObjects(unittest.TestCase): @@ -120,3 +120,12 @@ def test_string_name(self): s = StringName("A \"Quoted test\"") self.assertEqual(repr(s), "&\"A \\\"Quoted test\\\"\"") + + def test_typed_dictionary(self): + dict1 = { + StringName("asd"): GDObject("ExtResource", "2_qwert") + } + td = TypedDictionary("StringName", GDObject("ExtResource", "1_qwert"), dict1) + self.assertEqual(repr(td), """Dictionary[StringName, ExtResource("1_qwert")]({ +&"asd": ExtResource("2_qwert") +})""") diff --git a/tests/test_parser.py b/tests/test_parser.py index caaa312..ae87aa9 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -3,7 +3,7 @@ from pyparsing import ParseException -from godot_parser import GDFile, GDObject, GDSection, GDSectionHeader, Vector2, StringName, parse +from godot_parser import GDFile, GDObject, GDSection, GDSectionHeader, Vector2, StringName, parse, TypedDictionary HERE = os.path.dirname(__file__) @@ -129,6 +129,31 @@ ) ), ), + ( + """[sub_resource type="CustomType" id=1] + typed_dict_1 = Dictionary[StringName, ExtResource("1_testt")]({ + &"key": ExtResource("2_testt") + }) + typed_dict_2 = Dictionary[ExtResource("1_testt"), StringName]({ + ExtResource("2_testt"): &"key" + }) + """ , +GDFile( + GDSection( + GDSectionHeader("sub_resource", type="CustomType", id=1), + **{ + "typed_dict_1": TypedDictionary("StringName", GDObject("ExtResource", "1_testt"), + { + StringName("key"): GDObject("ExtResource", "2_testt") + }), + "typed_dict_2": TypedDictionary(GDObject("ExtResource", "1_testt"), "StringName", + { + GDObject("ExtResource", "2_testt"): StringName("key") + }) + } + ) + ), + ), ] From b9bbce2e88f84153b48bc4fa165bc389f3d842a6 Mon Sep 17 00:00:00 2001 From: DougVanny Date: Tue, 17 Feb 2026 20:25:51 -0300 Subject: [PATCH 08/21] Handling and testing special characters --- godot_parser/files.py | 2 +- godot_parser/util.py | 3 ++- tests/test_gdfile.py | 22 ++++++++++++++++++++++ tests/test_objects.py | 1 + tests/test_parser.py | 21 +++++++++++++++++++-- 5 files changed, 45 insertions(+), 4 deletions(-) diff --git a/godot_parser/files.py b/godot_parser/files.py index 87483f6..f903198 100644 --- a/godot_parser/files.py +++ b/godot_parser/files.py @@ -304,7 +304,7 @@ def get_node(self, path: str = ".") -> Optional[GDNodeSection]: @classmethod def parse(cls: Type[GDFileType], contents: str) -> GDFileType: """Parse the contents of a Godot file""" - return cls.from_parser(scene_file.parse_string(contents, parse_all=True)) + return cls.from_parser(scene_file.parse_with_tabs().parse_string(contents, parse_all=True)) @classmethod def load(cls: Type[GDFileType], filepath: str) -> GDFileType: diff --git a/godot_parser/util.py b/godot_parser/util.py index 76e4b56..622f410 100644 --- a/godot_parser/util.py +++ b/godot_parser/util.py @@ -10,7 +10,8 @@ def stringify_object(value): if value is None: return "null" elif isinstance(value, str): - return json.dumps(value, ensure_ascii=False) + #return json.dumps(value, ensure_ascii=False) + return "\"%s\"" % value.replace("\"", "\\\"") elif isinstance(value, bool): return "true" if value else "false" elif isinstance(value, dict): diff --git a/tests/test_gdfile.py b/tests/test_gdfile.py index f2e35dd..dd35655 100644 --- a/tests/test_gdfile.py +++ b/tests/test_gdfile.py @@ -242,3 +242,25 @@ def test_file_equality(self): resource = s1.find_section("resource") resource["key"] = "value" self.assertNotEqual(s1, s2) + + + + def test_string_special_characters(self): + """ + Testing strings with multiple special characters. Currently matching Godot 4.6 behavior + + Tab handling is done by calling parse_with_tabs before parse_string + For this reason, this test is being done at a GDFile level, where this method is called upon parsing + """ + res = GDResource() + res.add_section( + GDResourceSection( + str_value="\ta\"q\'é'd\"\n", + ) + ) + self.assertEqual(str(res), """[gd_resource load_steps=1 format=3] + +[resource] +str_value = " a\\"q'é'd\\" +" +""") \ No newline at end of file diff --git a/tests/test_objects.py b/tests/test_objects.py index aa3d72c..9b56b9d 100644 --- a/tests/test_objects.py +++ b/tests/test_objects.py @@ -122,6 +122,7 @@ def test_string_name(self): self.assertEqual(repr(s), "&\"A \\\"Quoted test\\\"\"") def test_typed_dictionary(self): + """Test for TypedDictionary""" dict1 = { StringName("asd"): GDObject("ExtResource", "2_qwert") } diff --git a/tests/test_parser.py b/tests/test_parser.py index ae87aa9..be7099a 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -116,8 +116,10 @@ string_name_value = &"StringName" string_quote = "\\"String\\"" string_name_quote = &"\\"StringName\\"" + string_single_quote = "\'String\'" + string_name_single_quote = &"\'StringName\'" """ , -GDFile( + GDFile( GDSection( GDSectionHeader("sub_resource", type="CustomType", id=1), **{ @@ -125,6 +127,8 @@ "string_name_value": StringName("StringName"), "string_quote": "\"String\"", "string_name_quote": StringName("\"StringName\""), + "string_single_quote": "'String'", + "string_name_single_quote": StringName("'StringName'"), } ) ), @@ -138,7 +142,7 @@ ExtResource("2_testt"): &"key" }) """ , -GDFile( + GDFile( GDSection( GDSectionHeader("sub_resource", type="CustomType", id=1), **{ @@ -154,6 +158,19 @@ ) ), ), + ( + """[node name="Label" type="Label" parent="." unique_id=1387035530] + text = "\ta\\\"q\\'é'd\\\"\n" + """, + GDFile( + GDSection( + GDSectionHeader("node", name="Label", type="Label", parent=".", unique_id=1387035530), + **{ + "text": "\ta\"q'é'd\"\n" + } + ) + ), + ), ] From 5dc96f70da4e6aa182118a6620afd3cf08f28cb1 Mon Sep 17 00:00:00 2001 From: DougVanny Date: Tue, 17 Feb 2026 20:29:51 -0300 Subject: [PATCH 09/21] Allow specifying format version on GDCommonFile creation --- godot_parser/files.py | 12 ++++++------ tests/test_gdfile.py | 6 +++--- tests/test_parser.py | 4 ++++ 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/godot_parser/files.py b/godot_parser/files.py index f903198..659c55e 100644 --- a/godot_parser/files.py +++ b/godot_parser/files.py @@ -356,9 +356,9 @@ def __ne__(self, other) -> bool: class GDCommonFile(GDFile): """Base class with common application logic for all Godot file types""" - def __init__(self, name: str, *sections: GDSection) -> None: + def __init__(self, name: str, *sections: GDSection, _format:int=2) -> None: super().__init__( - GDSection(GDSectionHeader(name, load_steps=1, format=2)), *sections + GDSection(GDSectionHeader(name, load_steps=1, format=_format)), *sections ) self.load_steps = ( 1 + len(self.get_ext_resources()) + len(self.get_sub_resources()) @@ -449,10 +449,10 @@ def _renumber_resource_ids( class GDScene(GDCommonFile): - def __init__(self, *sections: GDSection) -> None: - super().__init__("gd_scene", *sections) + def __init__(self, *sections: GDSection, _format: int = 2) -> None: + super().__init__("gd_scene", *sections, _format = _format) class GDResource(GDCommonFile): - def __init__(self, *sections: GDSection) -> None: - super().__init__("gd_resource", *sections) + def __init__(self, *sections: GDSection, _format: int = 2) -> None: + super().__init__("gd_resource", *sections, _format = _format) diff --git a/tests/test_gdfile.py b/tests/test_gdfile.py index dd35655..09cb091 100644 --- a/tests/test_gdfile.py +++ b/tests/test_gdfile.py @@ -13,7 +13,7 @@ def test_basic_scene(self): def test_all_data_types(self): """Run the parsing test cases""" - res = GDResource() + res = GDResource(_format = 3) res.add_section( GDResourceSection( list=[1, 2.0, "string"], @@ -24,7 +24,7 @@ def test_all_data_types(self): ) self.assertEqual( str(res), - """[gd_resource load_steps=1 format=2] + """[gd_resource load_steps=1 format=3] [resource] list = [ 1, 2.0, "string" ] @@ -258,7 +258,7 @@ def test_string_special_characters(self): str_value="\ta\"q\'é'd\"\n", ) ) - self.assertEqual(str(res), """[gd_resource load_steps=1 format=3] + self.assertEqual(str(res), """[gd_resource load_steps=1 format=2] [resource] str_value = " a\\"q'é'd\\" diff --git a/tests/test_parser.py b/tests/test_parser.py index be7099a..d4c107f 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -16,6 +16,10 @@ "[gd_resource load_steps=5 format=2]", GDFile(GDSection(GDSectionHeader("gd_resource", load_steps=5, format=2))), ), + ( + "[gd_resource format=3]", + GDFile(GDSection(GDSectionHeader("gd_resource", format=3))), + ), ( '[ext_resource path="res://Sample.tscn" type="PackedScene" id=1]', GDFile( From 7c2af0c9b7c4be7f2f67657b7dd8dd7ba83d878d Mon Sep 17 00:00:00 2001 From: DougVanny Date: Tue, 17 Feb 2026 22:30:29 -0300 Subject: [PATCH 10/21] Fixed escaping back slashes --- godot_parser/util.py | 2 +- tests/test_gdfile.py | 5 +++-- tests/test_parser.py | 4 ++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/godot_parser/util.py b/godot_parser/util.py index 622f410..468d1b7 100644 --- a/godot_parser/util.py +++ b/godot_parser/util.py @@ -11,7 +11,7 @@ def stringify_object(value): return "null" elif isinstance(value, str): #return json.dumps(value, ensure_ascii=False) - return "\"%s\"" % value.replace("\"", "\\\"") + return "\"%s\"" % value.replace("\\","\\\\").replace("\"", "\\\"") elif isinstance(value, bool): return "true" if value else "false" elif isinstance(value, dict): diff --git a/tests/test_gdfile.py b/tests/test_gdfile.py index 09cb091..f22d1a1 100644 --- a/tests/test_gdfile.py +++ b/tests/test_gdfile.py @@ -255,12 +255,13 @@ def test_string_special_characters(self): res = GDResource() res.add_section( GDResourceSection( - str_value="\ta\"q\'é'd\"\n", + str_value="\ta\"q\'é'd\"\n\n\\", ) ) self.assertEqual(str(res), """[gd_resource load_steps=1 format=2] [resource] str_value = " a\\"q'é'd\\" -" + +\\\\" """) \ No newline at end of file diff --git a/tests/test_parser.py b/tests/test_parser.py index d4c107f..0eee5b8 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -164,13 +164,13 @@ ), ( """[node name="Label" type="Label" parent="." unique_id=1387035530] - text = "\ta\\\"q\\'é'd\\\"\n" + text = "\ta\\\"q\\'é'd\\\"\n\n\\\\" """, GDFile( GDSection( GDSectionHeader("node", name="Label", type="Label", parent=".", unique_id=1387035530), **{ - "text": "\ta\"q'é'd\"\n" + "text": "\ta\"q'é'd\"\n\n\\" } ) ), From 9cd8e61ff6c0aaa5ae50197f0596667fe4da60aa Mon Sep 17 00:00:00 2001 From: DougVanny Date: Wed, 18 Feb 2026 00:50:39 -0300 Subject: [PATCH 11/21] test_parse_files.py now using difflib and dealing with whitespace differences between parsed and original files --- test_parse_files.py | 118 ++++++++++++++++++++++++++++++-------------- 1 file changed, 81 insertions(+), 37 deletions(-) diff --git a/test_parse_files.py b/test_parse_files.py index 0c0d0d5..39fd644 100755 --- a/test_parse_files.py +++ b/test_parse_files.py @@ -1,58 +1,102 @@ #!/usr/bin/env python import argparse import os -import sys import re -from itertools import zip_longest +import io +import sys +import traceback +import difflib from godot_parser import load, parse -# Regex to detect all whitespaces not between quotes -line_normalizer_re = re.compile('\\s+(?=((\\[\\"]|[^\\"])*"(\\[\\"]|[^\\"])*")*(\\[\\"]|[^\\"])*$)') +# Regex to detect space sequences +space_re = re.compile(r" +") +# Regex to detect all spaces not surrounded by alphanumeric characters +line_normalizer_re = re.compile(r"(?<=\W) +| +(?=\W)") +# Regex to detect quotes and possible escape sequences +find_quote_re = re.compile(r"(\\*\")") + +def join_lines_within_quotes(input: list[str]): + buffer_list = [] + lines = [] + buffer = "" + + for part in input: + # Find all quotes that are not escaped + # " is not escaped. \" is escaped. \\" is not escaped as \\ becomes \, leaving the quote unescaped + read_pos = 0 + for match in find_quote_re.finditer(part): + span = match.span() + match_text = part[span[0]:span[1]] + buffer += part[read_pos:span[1]] + read_pos = span[1] + if len(match_text)%2 == 1: + buffer_list.append(buffer) + buffer = "" + buffer += part[read_pos:] + + if (len(buffer_list) % 2 == 0) and buffer: + buffer_list.append(buffer) + buffer = "" + + if buffer: + buffer += "\n" + else: + for i in range(len(buffer_list)): + if i%2 == 0: + buffer_list[i] = space_re.sub(" ", buffer_list[i]) + buffer_list[i] = line_normalizer_re.sub("", buffer_list[i]) + lines.append("".join(buffer_list) + "\n") + buffer_list = [] + buffer = "" + + if buffer: + buffer_list.append(buffer) + if buffer_list: + for i in range(len(buffer_list)): + if i % 2 == 0: + buffer_list[i] = space_re.sub(" ", buffer_list[i]) + buffer_list[i] = line_normalizer_re.sub("", buffer_list[i]) + lines.append("".join(buffer_list) + "\n") -def _parse_and_test_file(filename: str) -> bool: - print("Parsing %s" % filename) + return lines + + +def _parse_and_test_file(filename: str, verbose: bool) -> bool: + if verbose: + print("Parsing %s" % filename) with open(filename, "r") as ifile: - contents = ifile.read() + original_file = ifile.read() try: - data = parse(contents) + parsed_file = str(parse(original_file)) except Exception: - print(" Parsing error!") - import traceback - - traceback.print_exc() + print("! Parsing error on %s" % filename, file=sys.stderr) + traceback.print_exc(file=sys.stderr) return False - f = load(filename) - with f.use_tree() as tree: - pass - - data_lines = [line_normalizer_re.sub("",l) for l in str(data).split("\n") if l] - content_lines = [line_normalizer_re.sub("",l) for l in contents.split("\n") if l] - if data_lines != content_lines: - print(" Error!") - max_len = max([len(l) for l in content_lines]) - if max_len < 100: - for orig, parsed in zip_longest(content_lines, data_lines, fillvalue=""): - c = " " if orig == parsed else "x" - print("%s <%s> %s" % (orig.ljust(max_len), c, parsed)) - else: - for orig, parsed in zip_longest( - content_lines, data_lines, fillvalue="----EMPTY----" - ): - c = " " if orig == parsed else "XXX)" - print("%s\n%s%s" % (orig, c, parsed)) - return False - return True + original_file = join_lines_within_quotes([l.strip() for l in io.StringIO(original_file).readlines() if l.strip()]) + parsed_file = join_lines_within_quotes([l.strip() for l in io.StringIO(str(parsed_file)).readlines() if l.strip()]) + + diff = difflib.context_diff(original_file, parsed_file, fromfile=filename, tofile="PARSED FILE") + diff = [" "+"\n ".join(l.strip().split("\n"))+"\n" for l in diff] + + if(len(diff) == 0): + return True + + print("! Difference detected on %s" % filename) + sys.stdout.writelines(diff) + return False def main(): """Test the parsing of one tscn file or all files in directory""" parser = argparse.ArgumentParser(description=main.__doc__) parser.add_argument("file_or_dir", help="Parse file or files under this directory") + parser.add_argument("--all", action='store_true', help="Tests all files even if one fails") + parser.add_argument("--verbose", "-v", action='store_true', help="Prints all file paths as they're parsed") args = parser.parse_args() if os.path.isfile(args.file_or_dir): - _parse_and_test_file(args.file_or_dir) + _parse_and_test_file(args.file_or_dir, args.verbose) else: for root, _dirs, files in os.walk(args.file_or_dir, topdown=False): for file in files: @@ -60,9 +104,9 @@ def main(): if ext not in [".tscn", ".tres"]: continue filepath = os.path.join(root, file) - if not _parse_and_test_file(filepath): - sys.exit(1) + if not _parse_and_test_file(filepath, args.verbose) and not args.all: + return 1 if __name__ == "__main__": - main() + sys.exit(main()) From eb0b7b45fac0709c97bc085373837d34c769c0dc Mon Sep 17 00:00:00 2001 From: DougVanny Date: Wed, 18 Feb 2026 01:38:33 -0300 Subject: [PATCH 12/21] TypedArray --- godot_parser/objects.py | 47 ++++++++++++++++++++++++++++++++++++++--- godot_parser/util.py | 2 +- godot_parser/values.py | 18 ++++++++++++++-- tests/test_gdfile.py | 4 ++-- tests/test_objects.py | 18 +++++++++++++++- tests/test_parser.py | 26 ++++++++++++++++++++++- 6 files changed, 105 insertions(+), 10 deletions(-) diff --git a/godot_parser/objects.py b/godot_parser/objects.py index d05a6bb..243ac5f 100644 --- a/godot_parser/objects.py +++ b/godot_parser/objects.py @@ -14,6 +14,7 @@ "ExtResource", "SubResource", "StringName", + "TypedArray", "TypedDictionary", ] @@ -252,6 +253,46 @@ def id(self, id: int) -> None: self.args[0] = id +class TypedArray(): + def __init__(self, type, list_) -> None: + self.name = "Array" + self.type = type + self.list_ = list_ + + @classmethod + def WithCustomName(cls: Type[TypedArray], name, type, list_) -> TypedArray: + custom_array = TypedArray(type, list_) + custom_array.name = name + return custom_array + + @classmethod + def from_parser(cls: Type[TypedArray], parse_result) -> TypedArray: + return TypedArray.WithCustomName(*parse_result) + + def __str__(self) -> str: + return "%s[%s](%s)" % ( + self.name, + self.type, + stringify_object(self.list_) + ) + + def __repr__(self) -> str: + return self.__str__() + + def __eq__(self, other) -> bool: + if not isinstance(other, TypedArray): + return False + return self.name == other.name and \ + self.type == other.type and \ + self.list_ == other.list_ + + def __ne__(self, other) -> bool: + return not self.__eq__(other) + + def __hash__(self): + return hash(frozenset((self.name,self.type,self.list_))) + + class TypedDictionary(): def __init__(self, key_type, value_type, dict_) -> None: self.name = "Dictionary" @@ -261,9 +302,9 @@ def __init__(self, key_type, value_type, dict_) -> None: @classmethod def WithCustomName(cls: Type[TypedDictionary], name, key_type, value_type, dict_) -> TypedDictionary: - custom_dict_ = TypedDictionary(key_type, value_type, dict_) - custom_dict_.name = name - return custom_dict_ + custom_dict = TypedDictionary(key_type, value_type, dict_) + custom_dict.name = name + return custom_dict @classmethod def from_parser(cls: Type[TypedDictionary], parse_result) -> TypedDictionary: diff --git a/godot_parser/util.py b/godot_parser/util.py index 468d1b7..ee4b2ae 100644 --- a/godot_parser/util.py +++ b/godot_parser/util.py @@ -23,7 +23,7 @@ def stringify_object(value): + "\n}" ) elif isinstance(value, list): - return "[ " + ", ".join([stringify_object(v) for v in value]) + " ]" + return "[" + ", ".join([stringify_object(v) for v in value]) + "]" else: return str(value) diff --git a/godot_parser/values.py b/godot_parser/values.py index f8b4b72..f6d8dbc 100644 --- a/godot_parser/values.py +++ b/godot_parser/values.py @@ -14,7 +14,7 @@ common, ) -from .objects import GDObject, StringName, TypedDictionary +from .objects import GDObject, StringName, TypedDictionary, TypedArray boolean = ( (Keyword("true") | Keyword("false")) @@ -53,6 +53,17 @@ .set_name("list") .set_parse_action(lambda p: p.as_list()) ) + +# Array[StringName]([&"a", &"b", &"c"]) +typed_list = ( + Word(alphas, alphanums).set_results_name("object_name") + + ( + Suppress("[") + + obj_type.set_results_name("type") + + Suppress("]") + ) + Suppress("(") + list_ + Suppress(")") +).set_parse_action(TypedArray.from_parser) + key_val = Group(value + Suppress(":") + value) # { @@ -64,6 +75,9 @@ .set_parse_action(lambda d: {k: v for k, v in d}) ) +# Dictionary[StringName,ExtResource("1_qwert")]({ +# &"_edit_use_anchors_": ExtResource("2_testt") +# }) typed_dict = ( Word(alphas, alphanums).set_results_name("object_name") + ( @@ -77,4 +91,4 @@ # Exports -value <<= list_ | dict_ | typed_dict | obj_ | primitive +value <<= list_ | typed_list | dict_ | typed_dict | obj_ | primitive diff --git a/tests/test_gdfile.py b/tests/test_gdfile.py index f22d1a1..bfd86c4 100644 --- a/tests/test_gdfile.py +++ b/tests/test_gdfile.py @@ -27,9 +27,9 @@ def test_all_data_types(self): """[gd_resource load_steps=1 format=3] [resource] -list = [ 1, 2.0, "string" ] +list = [1, 2.0, "string"] map = { -"key": [ "nested", Vector2(1, 1) ] +"key": ["nested", Vector2(1, 1)] } empty = null escaped = "foo(\\"bar\\")" diff --git a/tests/test_objects.py b/tests/test_objects.py index 9b56b9d..b3690b4 100644 --- a/tests/test_objects.py +++ b/tests/test_objects.py @@ -1,6 +1,6 @@ import unittest -from godot_parser import Color, ExtResource, NodePath, SubResource, Vector2, Vector3, StringName, GDObject, TypedDictionary +from godot_parser import Color, ExtResource, NodePath, SubResource, Vector2, Vector3, StringName, GDObject, TypedDictionary, TypedArray class TestGDObjects(unittest.TestCase): @@ -130,3 +130,19 @@ def test_typed_dictionary(self): self.assertEqual(repr(td), """Dictionary[StringName, ExtResource("1_qwert")]({ &"asd": ExtResource("2_qwert") })""") + + def test_typed_array(self): + """Test for TypedArray""" + list1 = [ + StringName("asd"), + StringName("dsa"), + ] + ta = TypedArray("StringName", list1) + self.assertEqual(repr(ta), """Array[StringName]([&"asd", &"dsa"])""") + + list2 = [ + GDObject("ExtResource", "1_qwert"), + GDObject("ExtResource", "2_testt") + ] + ta = TypedArray("StringName", list2) + self.assertEqual(repr(ta), """Array[StringName]([ExtResource("1_qwert"), ExtResource("2_testt")])""") diff --git a/tests/test_parser.py b/tests/test_parser.py index 0eee5b8..b55d76f 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -3,7 +3,7 @@ from pyparsing import ParseException -from godot_parser import GDFile, GDObject, GDSection, GDSectionHeader, Vector2, StringName, parse, TypedDictionary +from godot_parser import GDFile, GDObject, GDSection, GDSectionHeader, Vector2, StringName, parse, TypedDictionary, TypedArray HERE = os.path.dirname(__file__) @@ -162,6 +162,30 @@ ) ), ), + ( + """[sub_resource type="CustomType" id=1] + typed_array_1 = Array[StringName]([&"a", &"b", &"c"]) + typed_array_2 = Array[ExtResource("1_typee")]([ExtResource("1_qwert"), ExtResource("2_testt")]) + """ , + GDFile( + GDSection( + GDSectionHeader("sub_resource", type="CustomType", id=1), + **{ + "typed_array_1": TypedArray("StringName", + [ + StringName("a"), + StringName("b"), + StringName("c"), + ]), + "typed_array_2": TypedArray(GDObject("ExtResource", "1_typee"), + [ + GDObject("ExtResource", "1_qwert"), + GDObject("ExtResource", "2_testt"), + ]) + } + ) + ), + ), ( """[node name="Label" type="Label" parent="." unique_id=1387035530] text = "\ta\\\"q\\'é'd\\\"\n\n\\\\" From 31f177a9e8eb6faa29987c32f8fef4f7ddab1df3 Mon Sep 17 00:00:00 2001 From: DougVanny Date: Wed, 18 Feb 2026 01:56:11 -0300 Subject: [PATCH 13/21] string output fixes --- godot_parser/sections.py | 2 +- godot_parser/util.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/godot_parser/sections.py b/godot_parser/sections.py index 7f22f5a..0f51e9d 100644 --- a/godot_parser/sections.py +++ b/godot_parser/sections.py @@ -137,7 +137,7 @@ def __str__(self) -> str: if self.properties: ret += "\n" + "\n".join( [ - "%s = %s" % (k, stringify_object(v)) + "%s = %s" % ("\""+k+"\"" if ' ' in k else k, stringify_object(v)) for k, v in self.properties.items() ] ) diff --git a/godot_parser/util.py b/godot_parser/util.py index ee4b2ae..4604d16 100644 --- a/godot_parser/util.py +++ b/godot_parser/util.py @@ -15,6 +15,8 @@ def stringify_object(value): elif isinstance(value, bool): return "true" if value else "false" elif isinstance(value, dict): + if len(value) == 0: + return "{}" return ( "{\n" + ",\n".join( From 2fe6e00d20c1779050806ccbeef9c83f5b2c5791 Mon Sep 17 00:00:00 2001 From: DougVanny Date: Wed, 18 Feb 2026 02:00:40 -0300 Subject: [PATCH 14/21] Godot4 tests --- tests/test_gdfile.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/test_gdfile.py b/tests/test_gdfile.py index bfd86c4..612ac0d 100644 --- a/tests/test_gdfile.py +++ b/tests/test_gdfile.py @@ -243,8 +243,7 @@ def test_file_equality(self): resource["key"] = "value" self.assertNotEqual(s1, s2) - - +class Godot4Test(unittest.TestCase): def test_string_special_characters(self): """ Testing strings with multiple special characters. Currently matching Godot 4.6 behavior From 939a3c2a1fc5e468446d275d560e5d3349fe6305 Mon Sep 17 00:00:00 2001 From: DougVanny Date: Wed, 18 Feb 2026 02:13:30 -0300 Subject: [PATCH 15/21] Unescaping test_parse_files output --- test_parse_files.py | 31 ++++++++++++++++++++++++------- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/test_parse_files.py b/test_parse_files.py index 39fd644..0abf7e0 100755 --- a/test_parse_files.py +++ b/test_parse_files.py @@ -16,7 +16,7 @@ # Regex to detect quotes and possible escape sequences find_quote_re = re.compile(r"(\\*\")") -def join_lines_within_quotes(input: list[str]): +def join_lines_within_quotes(input: list[str], unescape: bool): buffer_list = [] lines = [] buffer = "" @@ -46,6 +46,8 @@ def join_lines_within_quotes(input: list[str]): if i%2 == 0: buffer_list[i] = space_re.sub(" ", buffer_list[i]) buffer_list[i] = line_normalizer_re.sub("", buffer_list[i]) + elif unescape: + buffer_list[i] = buffer_list[i].encode('latin-1', 'backslashreplace').decode('unicode-escape') lines.append("".join(buffer_list) + "\n") buffer_list = [] buffer = "" @@ -57,12 +59,14 @@ def join_lines_within_quotes(input: list[str]): if i % 2 == 0: buffer_list[i] = space_re.sub(" ", buffer_list[i]) buffer_list[i] = line_normalizer_re.sub("", buffer_list[i]) + elif unescape: + buffer_list[i] = buffer_list[i].encode('latin-1', 'backslashreplace').decode('unicode-escape') lines.append("".join(buffer_list) + "\n") return lines -def _parse_and_test_file(filename: str, verbose: bool) -> bool: +def _parse_and_test_file(filename: str, verbose: bool, unescape: bool) -> bool: if verbose: print("Parsing %s" % filename) with open(filename, "r") as ifile: @@ -74,8 +78,14 @@ def _parse_and_test_file(filename: str, verbose: bool) -> bool: traceback.print_exc(file=sys.stderr) return False - original_file = join_lines_within_quotes([l.strip() for l in io.StringIO(original_file).readlines() if l.strip()]) - parsed_file = join_lines_within_quotes([l.strip() for l in io.StringIO(str(parsed_file)).readlines() if l.strip()]) + original_file = join_lines_within_quotes( + [l.strip() for l in io.StringIO(original_file).readlines() if l.strip()], + unescape + ) + parsed_file = join_lines_within_quotes( + [l.strip() for l in io.StringIO(str(parsed_file)).readlines() if l.strip()], + unescape + ) diff = difflib.context_diff(original_file, parsed_file, fromfile=filename, tofile="PARSED FILE") diff = [" "+"\n ".join(l.strip().split("\n"))+"\n" for l in diff] @@ -94,18 +104,25 @@ def main(): parser.add_argument("file_or_dir", help="Parse file or files under this directory") parser.add_argument("--all", action='store_true', help="Tests all files even if one fails") parser.add_argument("--verbose", "-v", action='store_true', help="Prints all file paths as they're parsed") + parser.add_argument("--unescape", action='store_true', help="Attempts to unescape strings before comparison (Godot 4.5+ standard)") args = parser.parse_args() if os.path.isfile(args.file_or_dir): - _parse_and_test_file(args.file_or_dir, args.verbose) + _parse_and_test_file(args.file_or_dir, args.verbose, args.unescape) else: + all_passed = True for root, _dirs, files in os.walk(args.file_or_dir, topdown=False): for file in files: ext = os.path.splitext(file)[1] if ext not in [".tscn", ".tres"]: continue filepath = os.path.join(root, file) - if not _parse_and_test_file(filepath, args.verbose) and not args.all: - return 1 + if not _parse_and_test_file(filepath, args.verbose, args.unescape): + all_passed = False + if not args.all: + return 1 + + if all_passed: + print("All tests passed!") if __name__ == "__main__": From d63e13194aba7c3c3a04932602007d395a7f5151 Mon Sep 17 00:00:00 2001 From: DougVanny Date: Wed, 18 Feb 2026 02:16:39 -0300 Subject: [PATCH 16/21] Removed format on GDCommonFile as this is just a small part of a bigger change --- godot_parser/files.py | 12 ++++++------ tests/test_gdfile.py | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/godot_parser/files.py b/godot_parser/files.py index 659c55e..f903198 100644 --- a/godot_parser/files.py +++ b/godot_parser/files.py @@ -356,9 +356,9 @@ def __ne__(self, other) -> bool: class GDCommonFile(GDFile): """Base class with common application logic for all Godot file types""" - def __init__(self, name: str, *sections: GDSection, _format:int=2) -> None: + def __init__(self, name: str, *sections: GDSection) -> None: super().__init__( - GDSection(GDSectionHeader(name, load_steps=1, format=_format)), *sections + GDSection(GDSectionHeader(name, load_steps=1, format=2)), *sections ) self.load_steps = ( 1 + len(self.get_ext_resources()) + len(self.get_sub_resources()) @@ -449,10 +449,10 @@ def _renumber_resource_ids( class GDScene(GDCommonFile): - def __init__(self, *sections: GDSection, _format: int = 2) -> None: - super().__init__("gd_scene", *sections, _format = _format) + def __init__(self, *sections: GDSection) -> None: + super().__init__("gd_scene", *sections) class GDResource(GDCommonFile): - def __init__(self, *sections: GDSection, _format: int = 2) -> None: - super().__init__("gd_resource", *sections, _format = _format) + def __init__(self, *sections: GDSection) -> None: + super().__init__("gd_resource", *sections) diff --git a/tests/test_gdfile.py b/tests/test_gdfile.py index 612ac0d..0461d30 100644 --- a/tests/test_gdfile.py +++ b/tests/test_gdfile.py @@ -13,7 +13,7 @@ def test_basic_scene(self): def test_all_data_types(self): """Run the parsing test cases""" - res = GDResource(_format = 3) + res = GDResource() res.add_section( GDResourceSection( list=[1, 2.0, "string"], @@ -24,7 +24,7 @@ def test_all_data_types(self): ) self.assertEqual( str(res), - """[gd_resource load_steps=1 format=3] + """[gd_resource load_steps=1 format=2] [resource] list = [1, 2.0, "string"] From 5f360bc30dd285355065a649fc5b1fbc012fd2c4 Mon Sep 17 00:00:00 2001 From: DougVanny Date: Wed, 18 Feb 2026 02:24:35 -0300 Subject: [PATCH 17/21] Reverted test_parse_files.py --- test_parse_files.py | 132 ++++++++++++-------------------------------- 1 file changed, 34 insertions(+), 98 deletions(-) diff --git a/test_parse_files.py b/test_parse_files.py index 0abf7e0..bccb7b0 100755 --- a/test_parse_files.py +++ b/test_parse_files.py @@ -1,129 +1,65 @@ #!/usr/bin/env python import argparse import os -import re -import io import sys -import traceback -import difflib +from itertools import zip_longest from godot_parser import load, parse -# Regex to detect space sequences -space_re = re.compile(r" +") -# Regex to detect all spaces not surrounded by alphanumeric characters -line_normalizer_re = re.compile(r"(?<=\W) +| +(?=\W)") -# Regex to detect quotes and possible escape sequences -find_quote_re = re.compile(r"(\\*\")") -def join_lines_within_quotes(input: list[str], unescape: bool): - buffer_list = [] - lines = [] - buffer = "" - - for part in input: - # Find all quotes that are not escaped - # " is not escaped. \" is escaped. \\" is not escaped as \\ becomes \, leaving the quote unescaped - read_pos = 0 - for match in find_quote_re.finditer(part): - span = match.span() - match_text = part[span[0]:span[1]] - buffer += part[read_pos:span[1]] - read_pos = span[1] - if len(match_text)%2 == 1: - buffer_list.append(buffer) - buffer = "" - buffer += part[read_pos:] - - if (len(buffer_list) % 2 == 0) and buffer: - buffer_list.append(buffer) - buffer = "" - - if buffer: - buffer += "\n" - else: - for i in range(len(buffer_list)): - if i%2 == 0: - buffer_list[i] = space_re.sub(" ", buffer_list[i]) - buffer_list[i] = line_normalizer_re.sub("", buffer_list[i]) - elif unescape: - buffer_list[i] = buffer_list[i].encode('latin-1', 'backslashreplace').decode('unicode-escape') - lines.append("".join(buffer_list) + "\n") - buffer_list = [] - buffer = "" - - if buffer: - buffer_list.append(buffer) - if buffer_list: - for i in range(len(buffer_list)): - if i % 2 == 0: - buffer_list[i] = space_re.sub(" ", buffer_list[i]) - buffer_list[i] = line_normalizer_re.sub("", buffer_list[i]) - elif unescape: - buffer_list[i] = buffer_list[i].encode('latin-1', 'backslashreplace').decode('unicode-escape') - lines.append("".join(buffer_list) + "\n") - - return lines - - -def _parse_and_test_file(filename: str, verbose: bool, unescape: bool) -> bool: - if verbose: - print("Parsing %s" % filename) +def _parse_and_test_file(filename: str) -> bool: + print("Parsing %s" % filename) with open(filename, "r") as ifile: - original_file = ifile.read() + contents = ifile.read() try: - parsed_file = str(parse(original_file)) + data = parse(contents) except Exception: - print("! Parsing error on %s" % filename, file=sys.stderr) - traceback.print_exc(file=sys.stderr) - return False - - original_file = join_lines_within_quotes( - [l.strip() for l in io.StringIO(original_file).readlines() if l.strip()], - unescape - ) - parsed_file = join_lines_within_quotes( - [l.strip() for l in io.StringIO(str(parsed_file)).readlines() if l.strip()], - unescape - ) + print(" Parsing error!") + import traceback - diff = difflib.context_diff(original_file, parsed_file, fromfile=filename, tofile="PARSED FILE") - diff = [" "+"\n ".join(l.strip().split("\n"))+"\n" for l in diff] - - if(len(diff) == 0): - return True + traceback.print_exc() + return False - print("! Difference detected on %s" % filename) - sys.stdout.writelines(diff) - return False + f = load(filename) + with f.use_tree() as tree: + pass + + data_lines = [l for l in str(data).split("\n") if l] + content_lines = [l for l in contents.split("\n") if l] + if data_lines != content_lines: + print(" Error!") + max_len = max([len(l) for l in content_lines]) + if max_len < 100: + for orig, parsed in zip_longest(content_lines, data_lines, fillvalue=""): + c = " " if orig == parsed else "x" + print("%s <%s> %s" % (orig.ljust(max_len), c, parsed)) + else: + for orig, parsed in zip_longest( + content_lines, data_lines, fillvalue="----EMPTY----" + ): + c = " " if orig == parsed else "XXX)" + print("%s\n%s%s" % (orig, c, parsed)) + return False + return True def main(): """Test the parsing of one tscn file or all files in directory""" parser = argparse.ArgumentParser(description=main.__doc__) parser.add_argument("file_or_dir", help="Parse file or files under this directory") - parser.add_argument("--all", action='store_true', help="Tests all files even if one fails") - parser.add_argument("--verbose", "-v", action='store_true', help="Prints all file paths as they're parsed") - parser.add_argument("--unescape", action='store_true', help="Attempts to unescape strings before comparison (Godot 4.5+ standard)") args = parser.parse_args() if os.path.isfile(args.file_or_dir): - _parse_and_test_file(args.file_or_dir, args.verbose, args.unescape) + _parse_and_test_file(args.file_or_dir) else: - all_passed = True for root, _dirs, files in os.walk(args.file_or_dir, topdown=False): for file in files: ext = os.path.splitext(file)[1] if ext not in [".tscn", ".tres"]: continue filepath = os.path.join(root, file) - if not _parse_and_test_file(filepath, args.verbose, args.unescape): - all_passed = False - if not args.all: - return 1 - - if all_passed: - print("All tests passed!") + if not _parse_and_test_file(filepath): + sys.exit(1) if __name__ == "__main__": - sys.exit(main()) + main() From 10371433f4d61760c8f92cabb42e51635bccb09e Mon Sep 17 00:00:00 2001 From: DougVanny Date: Wed, 18 Feb 2026 02:26:41 -0300 Subject: [PATCH 18/21] Reverted string print to Godot3 format --- godot_parser/util.py | 3 +-- tests/test_gdfile.py | 22 ---------------------- 2 files changed, 1 insertion(+), 24 deletions(-) diff --git a/godot_parser/util.py b/godot_parser/util.py index 4604d16..356d25b 100644 --- a/godot_parser/util.py +++ b/godot_parser/util.py @@ -10,8 +10,7 @@ def stringify_object(value): if value is None: return "null" elif isinstance(value, str): - #return json.dumps(value, ensure_ascii=False) - return "\"%s\"" % value.replace("\\","\\\\").replace("\"", "\\\"") + return json.dumps(value, ensure_ascii=False) elif isinstance(value, bool): return "true" if value else "false" elif isinstance(value, dict): diff --git a/tests/test_gdfile.py b/tests/test_gdfile.py index 0461d30..8715664 100644 --- a/tests/test_gdfile.py +++ b/tests/test_gdfile.py @@ -242,25 +242,3 @@ def test_file_equality(self): resource = s1.find_section("resource") resource["key"] = "value" self.assertNotEqual(s1, s2) - -class Godot4Test(unittest.TestCase): - def test_string_special_characters(self): - """ - Testing strings with multiple special characters. Currently matching Godot 4.6 behavior - - Tab handling is done by calling parse_with_tabs before parse_string - For this reason, this test is being done at a GDFile level, where this method is called upon parsing - """ - res = GDResource() - res.add_section( - GDResourceSection( - str_value="\ta\"q\'é'd\"\n\n\\", - ) - ) - self.assertEqual(str(res), """[gd_resource load_steps=1 format=2] - -[resource] -str_value = " a\\"q'é'd\\" - -\\\\" -""") \ No newline at end of file From d3e5cdf114ca770f50a7cf5a993c62eace8dd4d0 Mon Sep 17 00:00:00 2001 From: DougVanny Date: Sun, 22 Feb 2026 20:24:28 -0300 Subject: [PATCH 19/21] Add TypeVar definitions --- godot_parser/objects.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/godot_parser/objects.py b/godot_parser/objects.py index 243ac5f..539e421 100644 --- a/godot_parser/objects.py +++ b/godot_parser/objects.py @@ -253,6 +253,8 @@ def id(self, id: int) -> None: self.args[0] = id +TypedArrayType = TypeVar("TypedArrayType", bound="TypedArray") + class TypedArray(): def __init__(self, type, list_) -> None: self.name = "Array" @@ -260,13 +262,13 @@ def __init__(self, type, list_) -> None: self.list_ = list_ @classmethod - def WithCustomName(cls: Type[TypedArray], name, type, list_) -> TypedArray: + def WithCustomName(cls: Type[TypedArrayType], name, type, list_) -> TypedArrayType: custom_array = TypedArray(type, list_) custom_array.name = name return custom_array @classmethod - def from_parser(cls: Type[TypedArray], parse_result) -> TypedArray: + def from_parser(cls: Type[TypedArrayType], parse_result) -> TypedArrayType: return TypedArray.WithCustomName(*parse_result) def __str__(self) -> str: @@ -293,6 +295,8 @@ def __hash__(self): return hash(frozenset((self.name,self.type,self.list_))) +TypedDictionaryType = TypeVar("TypedDictionaryType", bound="TypedDictionary") + class TypedDictionary(): def __init__(self, key_type, value_type, dict_) -> None: self.name = "Dictionary" @@ -301,13 +305,13 @@ def __init__(self, key_type, value_type, dict_) -> None: self.dict_ = dict_ @classmethod - def WithCustomName(cls: Type[TypedDictionary], name, key_type, value_type, dict_) -> TypedDictionary: + def WithCustomName(cls: Type[TypedDictionaryType], name, key_type, value_type, dict_) -> TypedDictionaryType: custom_dict = TypedDictionary(key_type, value_type, dict_) custom_dict.name = name return custom_dict @classmethod - def from_parser(cls: Type[TypedDictionary], parse_result) -> TypedDictionary: + def from_parser(cls: Type[TypedDictionaryType], parse_result) -> TypedDictionaryType: return TypedDictionary.WithCustomName(*parse_result) def __str__(self) -> str: @@ -335,12 +339,14 @@ def __ne__(self, other) -> bool: def __hash__(self): return hash(frozenset((self.name,self.key_type,self.value_type,self.dict_))) +StringNameType = TypeVar("StringNameType", bound="StringName") + class StringName(): def __init__(self, str) -> None: self.str = str @classmethod - def from_parser(cls: Type[StringName], parse_result) -> StringName: + def from_parser(cls: Type[StringNameType], parse_result) -> StringNameType: return StringName(parse_result[0]) def __str__(self) -> str: From 17101d7db748b8655971c92c56eda5a590fdb9a4 Mon Sep 17 00:00:00 2001 From: DougVanny Date: Sun, 22 Feb 2026 20:36:29 -0300 Subject: [PATCH 20/21] Type fix attempt 2 --- godot_parser/objects.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/godot_parser/objects.py b/godot_parser/objects.py index 539e421..c6b899a 100644 --- a/godot_parser/objects.py +++ b/godot_parser/objects.py @@ -262,13 +262,13 @@ def __init__(self, type, list_) -> None: self.list_ = list_ @classmethod - def WithCustomName(cls: Type[TypedArrayType], name, type, list_) -> TypedArrayType: + def WithCustomName(cls: Type[TypedArrayType], name, type, list_) -> "TypedArray": custom_array = TypedArray(type, list_) custom_array.name = name return custom_array @classmethod - def from_parser(cls: Type[TypedArrayType], parse_result) -> TypedArrayType: + def from_parser(cls: Type[TypedArrayType], parse_result) -> "TypedArray": return TypedArray.WithCustomName(*parse_result) def __str__(self) -> str: @@ -305,13 +305,13 @@ def __init__(self, key_type, value_type, dict_) -> None: self.dict_ = dict_ @classmethod - def WithCustomName(cls: Type[TypedDictionaryType], name, key_type, value_type, dict_) -> TypedDictionaryType: + def WithCustomName(cls: Type[TypedDictionaryType], name, key_type, value_type, dict_) -> "TypedDictionary": custom_dict = TypedDictionary(key_type, value_type, dict_) custom_dict.name = name return custom_dict @classmethod - def from_parser(cls: Type[TypedDictionaryType], parse_result) -> TypedDictionaryType: + def from_parser(cls: Type[TypedDictionaryType], parse_result) -> "TypedDictionary": return TypedDictionary.WithCustomName(*parse_result) def __str__(self) -> str: @@ -346,7 +346,7 @@ def __init__(self, str) -> None: self.str = str @classmethod - def from_parser(cls: Type[StringNameType], parse_result) -> StringNameType: + def from_parser(cls: Type[StringNameType], parse_result) -> "StringName": return StringName(parse_result[0]) def __str__(self) -> str: From 049de7933aa13c646a219b2942ee9c79f5e83d82 Mon Sep 17 00:00:00 2001 From: dougVanny Date: Sun, 22 Feb 2026 20:58:59 -0300 Subject: [PATCH 21/21] Fixes formatting * Fix 2 * Update formatting * Formatting fix 2 * More formatting * Fixes 100% --------- Co-authored-by: DougVanny --- godot_parser/files.py | 5 +- godot_parser/objects.py | 57 ++++++++++---------- godot_parser/sections.py | 3 +- godot_parser/util.py | 5 +- godot_parser/values.py | 32 +++++------ tests/test_objects.py | 40 +++++++++----- tests/test_parser.py | 114 ++++++++++++++++++++++----------------- tests/test_tree.py | 18 +++---- 8 files changed, 149 insertions(+), 125 deletions(-) diff --git a/godot_parser/files.py b/godot_parser/files.py index f903198..b5344ae 100644 --- a/godot_parser/files.py +++ b/godot_parser/files.py @@ -38,7 +38,6 @@ "editable", ] - GDFileType = TypeVar("GDFileType", bound="GDFile") @@ -304,7 +303,9 @@ def get_node(self, path: str = ".") -> Optional[GDNodeSection]: @classmethod def parse(cls: Type[GDFileType], contents: str) -> GDFileType: """Parse the contents of a Godot file""" - return cls.from_parser(scene_file.parse_with_tabs().parse_string(contents, parse_all=True)) + return cls.from_parser( + scene_file.parse_with_tabs().parse_string(contents, parse_all=True) + ) @classmethod def load(cls: Type[GDFileType], filepath: str) -> GDFileType: diff --git a/godot_parser/objects.py b/godot_parser/objects.py index c6b899a..0170cb6 100644 --- a/godot_parser/objects.py +++ b/godot_parser/objects.py @@ -75,7 +75,7 @@ def __ne__(self, other) -> bool: return not self.__eq__(other) def __hash__(self): - return hash(frozenset((self.name,*self.args))) + return hash(frozenset((self.name, *self.args))) class Vector2(GDObject): @@ -253,30 +253,24 @@ def id(self, id: int) -> None: self.args[0] = id -TypedArrayType = TypeVar("TypedArrayType", bound="TypedArray") - -class TypedArray(): +class TypedArray: def __init__(self, type, list_) -> None: self.name = "Array" self.type = type self.list_ = list_ @classmethod - def WithCustomName(cls: Type[TypedArrayType], name, type, list_) -> "TypedArray": + def WithCustomName(cls: Type["TypedArray"], name, type, list_) -> "TypedArray": custom_array = TypedArray(type, list_) custom_array.name = name return custom_array @classmethod - def from_parser(cls: Type[TypedArrayType], parse_result) -> "TypedArray": + def from_parser(cls: Type["TypedArray"], parse_result) -> "TypedArray": return TypedArray.WithCustomName(*parse_result) def __str__(self) -> str: - return "%s[%s](%s)" % ( - self.name, - self.type, - stringify_object(self.list_) - ) + return "%s[%s](%s)" % (self.name, self.type, stringify_object(self.list_)) def __repr__(self) -> str: return self.__str__() @@ -284,20 +278,20 @@ def __repr__(self) -> str: def __eq__(self, other) -> bool: if not isinstance(other, TypedArray): return False - return self.name == other.name and \ - self.type == other.type and \ - self.list_ == other.list_ + return ( + self.name == other.name + and self.type == other.type + and self.list_ == other.list_ + ) def __ne__(self, other) -> bool: return not self.__eq__(other) def __hash__(self): - return hash(frozenset((self.name,self.type,self.list_))) - + return hash(frozenset((self.name, self.type, self.list_))) -TypedDictionaryType = TypeVar("TypedDictionaryType", bound="TypedDictionary") -class TypedDictionary(): +class TypedDictionary: def __init__(self, key_type, value_type, dict_) -> None: self.name = "Dictionary" self.key_type = key_type @@ -305,13 +299,15 @@ def __init__(self, key_type, value_type, dict_) -> None: self.dict_ = dict_ @classmethod - def WithCustomName(cls: Type[TypedDictionaryType], name, key_type, value_type, dict_) -> "TypedDictionary": + def WithCustomName( + cls: Type["TypedDictionary"], name, key_type, value_type, dict_ + ) -> "TypedDictionary": custom_dict = TypedDictionary(key_type, value_type, dict_) custom_dict.name = name return custom_dict @classmethod - def from_parser(cls: Type[TypedDictionaryType], parse_result) -> "TypedDictionary": + def from_parser(cls: Type["TypedDictionary"], parse_result) -> "TypedDictionary": return TypedDictionary.WithCustomName(*parse_result) def __str__(self) -> str: @@ -319,7 +315,7 @@ def __str__(self) -> str: self.name, self.key_type, self.value_type, - stringify_object(self.dict_) + stringify_object(self.dict_), ) def __repr__(self) -> str: @@ -328,25 +324,26 @@ def __repr__(self) -> str: def __eq__(self, other) -> bool: if not isinstance(other, TypedDictionary): return False - return self.name == other.name and \ - self.key_type == other.key_type and \ - self.value_type == other.value_type and \ - self.dict_ == other.dict_ + return ( + self.name == other.name + and self.key_type == other.key_type + and self.value_type == other.value_type + and self.dict_ == other.dict_ + ) def __ne__(self, other) -> bool: return not self.__eq__(other) def __hash__(self): - return hash(frozenset((self.name,self.key_type,self.value_type,self.dict_))) + return hash(frozenset((self.name, self.key_type, self.value_type, self.dict_))) -StringNameType = TypeVar("StringNameType", bound="StringName") -class StringName(): +class StringName: def __init__(self, str) -> None: self.str = str @classmethod - def from_parser(cls: Type[StringNameType], parse_result) -> "StringName": + def from_parser(cls: Type["StringName"], parse_result) -> "StringName": return StringName(parse_result[0]) def __str__(self) -> str: @@ -364,4 +361,4 @@ def __ne__(self, other) -> bool: return not self.__eq__(other) def __hash__(self): - return hash(self.str) \ No newline at end of file + return hash(self.str) diff --git a/godot_parser/sections.py b/godot_parser/sections.py index 0f51e9d..5c6f152 100644 --- a/godot_parser/sections.py +++ b/godot_parser/sections.py @@ -14,7 +14,6 @@ "GDResourceSection", ] - GD_SECTION_REGISTRY = {} @@ -137,7 +136,7 @@ def __str__(self) -> str: if self.properties: ret += "\n" + "\n".join( [ - "%s = %s" % ("\""+k+"\"" if ' ' in k else k, stringify_object(v)) + "%s = %s" % ('"' + k + '"' if " " in k else k, stringify_object(v)) for k, v in self.properties.items() ] ) diff --git a/godot_parser/util.py b/godot_parser/util.py index 356d25b..5728969 100644 --- a/godot_parser/util.py +++ b/godot_parser/util.py @@ -19,7 +19,10 @@ def stringify_object(value): return ( "{\n" + ",\n".join( - ['%s: %s' % (stringify_object(k), stringify_object(v)) for k, v in value.items()] + [ + "%s: %s" % (stringify_object(k), stringify_object(v)) + for k, v in value.items() + ] ) + "\n}" ) diff --git a/godot_parser/values.py b/godot_parser/values.py index f6d8dbc..ab81265 100644 --- a/godot_parser/values.py +++ b/godot_parser/values.py @@ -14,7 +14,7 @@ common, ) -from .objects import GDObject, StringName, TypedDictionary, TypedArray +from .objects import GDObject, StringName, TypedArray, TypedDictionary boolean = ( (Keyword("true") | Keyword("false")) @@ -27,12 +27,12 @@ _string = QuotedString('"', escChar="\\", multiline=True).set_name("string") _string_name = ( - Suppress('&') + _string -).set_name("string_name").set_parse_action(StringName.from_parser) - -primitive = ( - null | _string | _string_name | boolean | common.number + (Suppress("&") + _string) + .set_name("string_name") + .set_parse_action(StringName.from_parser) ) + +primitive = null | _string | _string_name | boolean | common.number value = Forward() # Vector2( 1, 2 ) @@ -56,12 +56,11 @@ # Array[StringName]([&"a", &"b", &"c"]) typed_list = ( - Word(alphas, alphanums).set_results_name("object_name") + - ( - Suppress("[") - + obj_type.set_results_name("type") - + Suppress("]") - ) + Suppress("(") + list_ + Suppress(")") + Word(alphas, alphanums).set_results_name("object_name") + + (Suppress("[") + obj_type.set_results_name("type") + Suppress("]")) + + Suppress("(") + + list_ + + Suppress(")") ).set_parse_action(TypedArray.from_parser) key_val = Group(value + Suppress(":") + value) @@ -79,14 +78,17 @@ # &"_edit_use_anchors_": ExtResource("2_testt") # }) typed_dict = ( - Word(alphas, alphanums).set_results_name("object_name") + - ( + Word(alphas, alphanums).set_results_name("object_name") + + ( Suppress("[") + obj_type.set_results_name("key_type") + Suppress(",") + obj_type.set_results_name("value_type") + Suppress("]") - ) + Suppress("(") + dict_ + Suppress(")") + ) + + Suppress("(") + + dict_ + + Suppress(")") ).set_parse_action(TypedDictionary.from_parser) # Exports diff --git a/tests/test_objects.py b/tests/test_objects.py index b3690b4..969f38e 100644 --- a/tests/test_objects.py +++ b/tests/test_objects.py @@ -1,6 +1,17 @@ import unittest -from godot_parser import Color, ExtResource, NodePath, SubResource, Vector2, Vector3, StringName, GDObject, TypedDictionary, TypedArray +from godot_parser import ( + Color, + ExtResource, + GDObject, + NodePath, + StringName, + SubResource, + TypedArray, + TypedDictionary, + Vector2, + Vector3, +) class TestGDObjects(unittest.TestCase): @@ -112,24 +123,25 @@ def test_dunder(self): def test_string_name(self): """Test for StringName""" s = StringName("test") - self.assertEqual(repr(s), "&\"test\"") + self.assertEqual(repr(s), '&"test"') s2 = StringName("test") self.assertEqual(s, s2) s2.str = "bad" self.assertNotEqual(s, s2) - s = StringName("A \"Quoted test\"") - self.assertEqual(repr(s), "&\"A \\\"Quoted test\\\"\"") + s = StringName('A "Quoted test"') + self.assertEqual(repr(s), '&"A \\"Quoted test\\""') def test_typed_dictionary(self): """Test for TypedDictionary""" - dict1 = { - StringName("asd"): GDObject("ExtResource", "2_qwert") - } + dict1 = {StringName("asd"): GDObject("ExtResource", "2_qwert")} td = TypedDictionary("StringName", GDObject("ExtResource", "1_qwert"), dict1) - self.assertEqual(repr(td), """Dictionary[StringName, ExtResource("1_qwert")]({ + self.assertEqual( + repr(td), + """Dictionary[StringName, ExtResource("1_qwert")]({ &"asd": ExtResource("2_qwert") -})""") +})""", + ) def test_typed_array(self): """Test for TypedArray""" @@ -140,9 +152,9 @@ def test_typed_array(self): ta = TypedArray("StringName", list1) self.assertEqual(repr(ta), """Array[StringName]([&"asd", &"dsa"])""") - list2 = [ - GDObject("ExtResource", "1_qwert"), - GDObject("ExtResource", "2_testt") - ] + list2 = [GDObject("ExtResource", "1_qwert"), GDObject("ExtResource", "2_testt")] ta = TypedArray("StringName", list2) - self.assertEqual(repr(ta), """Array[StringName]([ExtResource("1_qwert"), ExtResource("2_testt")])""") + self.assertEqual( + repr(ta), + """Array[StringName]([ExtResource("1_qwert"), ExtResource("2_testt")])""", + ) diff --git a/tests/test_parser.py b/tests/test_parser.py index b55d76f..575ac5b 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -3,7 +3,17 @@ from pyparsing import ParseException -from godot_parser import GDFile, GDObject, GDSection, GDSectionHeader, Vector2, StringName, parse, TypedDictionary, TypedArray +from godot_parser import ( + GDFile, + GDObject, + GDSection, + GDSectionHeader, + StringName, + TypedArray, + TypedDictionary, + Vector2, + parse, +) HERE = os.path.dirname(__file__) @@ -115,22 +125,22 @@ ), ), ( - """[sub_resource type="CustomType" id=1] - string_value = "String" - string_name_value = &"StringName" - string_quote = "\\"String\\"" - string_name_quote = &"\\"StringName\\"" - string_single_quote = "\'String\'" - string_name_single_quote = &"\'StringName\'" - """ , + """[sub_resource type="CustomType" id=1] + string_value = "String" + string_name_value = &"StringName" + string_quote = "\\"String\\"" + string_name_quote = &"\\"StringName\\"" + string_single_quote = "\'String\'" + string_name_single_quote = &"\'StringName\'" + """, GDFile( GDSection( GDSectionHeader("sub_resource", type="CustomType", id=1), **{ "string_value": "String", "string_name_value": StringName("StringName"), - "string_quote": "\"String\"", - "string_name_quote": StringName("\"StringName\""), + "string_quote": '"String"', + "string_name_quote": StringName('"StringName"'), "string_single_quote": "'String'", "string_name_single_quote": StringName("'StringName'"), } @@ -138,64 +148,70 @@ ), ), ( - """[sub_resource type="CustomType" id=1] - typed_dict_1 = Dictionary[StringName, ExtResource("1_testt")]({ - &"key": ExtResource("2_testt") - }) - typed_dict_2 = Dictionary[ExtResource("1_testt"), StringName]({ - ExtResource("2_testt"): &"key" - }) - """ , + """[sub_resource type="CustomType" id=1] + typed_dict_1 = Dictionary[StringName, ExtResource("1_testt")]({ + &"key": ExtResource("2_testt") + }) + typed_dict_2 = Dictionary[ExtResource("1_testt"), StringName]({ + ExtResource("2_testt"): &"key" + }) + """, GDFile( GDSection( GDSectionHeader("sub_resource", type="CustomType", id=1), **{ - "typed_dict_1": TypedDictionary("StringName", GDObject("ExtResource", "1_testt"), - { - StringName("key"): GDObject("ExtResource", "2_testt") - }), - "typed_dict_2": TypedDictionary(GDObject("ExtResource", "1_testt"), "StringName", - { - GDObject("ExtResource", "2_testt"): StringName("key") - }) + "typed_dict_1": TypedDictionary( + "StringName", + GDObject("ExtResource", "1_testt"), + {StringName("key"): GDObject("ExtResource", "2_testt")}, + ), + "typed_dict_2": TypedDictionary( + GDObject("ExtResource", "1_testt"), + "StringName", + {GDObject("ExtResource", "2_testt"): StringName("key")}, + ), } ) ), ), ( - """[sub_resource type="CustomType" id=1] - typed_array_1 = Array[StringName]([&"a", &"b", &"c"]) - typed_array_2 = Array[ExtResource("1_typee")]([ExtResource("1_qwert"), ExtResource("2_testt")]) - """ , + """[sub_resource type="CustomType" id=1] + typed_array_1 = Array[StringName]([&"a", &"b", &"c"]) + typed_array_2 = Array[ExtResource("1_typee")]([ExtResource("1_qwert"), ExtResource("2_testt")]) + """, GDFile( GDSection( GDSectionHeader("sub_resource", type="CustomType", id=1), **{ - "typed_array_1": TypedArray("StringName", - [ - StringName("a"), - StringName("b"), - StringName("c"), - ]), - "typed_array_2": TypedArray(GDObject("ExtResource", "1_typee"), - [ - GDObject("ExtResource", "1_qwert"), - GDObject("ExtResource", "2_testt"), - ]) + "typed_array_1": TypedArray( + "StringName", + [ + StringName("a"), + StringName("b"), + StringName("c"), + ], + ), + "typed_array_2": TypedArray( + GDObject("ExtResource", "1_typee"), + [ + GDObject("ExtResource", "1_qwert"), + GDObject("ExtResource", "2_testt"), + ], + ), } ) ), ), ( - """[node name="Label" type="Label" parent="." unique_id=1387035530] - text = "\ta\\\"q\\'é'd\\\"\n\n\\\\" - """, + """[node name="Label" type="Label" parent="." unique_id=1387035530] + text = "\ta\\\"q\\'é'd\\\"\n\n\\\\" + """, GDFile( GDSection( - GDSectionHeader("node", name="Label", type="Label", parent=".", unique_id=1387035530), - **{ - "text": "\ta\"q'é'd\"\n\n\\" - } + GDSectionHeader( + "node", name="Label", type="Label", parent=".", unique_id=1387035530 + ), + **{"text": "\ta\"q'é'd\"\n\n\\"} ) ), ), diff --git a/tests/test_tree.py b/tests/test_tree.py index b0cf47b..d99ac7e 100644 --- a/tests/test_tree.py +++ b/tests/test_tree.py @@ -124,8 +124,7 @@ def setUpClass(cls): cls.root_scene = os.path.join(cls.project_dir, "Root.tscn") cls.mid_scene = os.path.join(cls.project_dir, "Mid.tscn") cls.leaf_scene = os.path.join(cls.project_dir, "Leaf.tscn") - scene = GDScene.parse( - """ + scene = GDScene.parse(""" [gd_scene load_steps=1 format=2] [node name="Root" type="KinematicBody2D"] collision_layer = 3 @@ -135,24 +134,20 @@ def setUpClass(cls): flip_h = false [node name="Health" type="Control" parent="."] [node name="LifeBar" type="TextureProgress" parent="Health"] -""" - ) +""") scene.write(cls.root_scene) - scene = GDScene.parse( - """ + scene = GDScene.parse(""" [gd_scene load_steps=2 format=2] [ext_resource path="res://Root.tscn" type="PackedScene" id=1] [node name="Mid" instance=ExtResource( 1 )] collision_layer = 4 [node name="Health" parent="." index="2"] pause_mode = 2 -""" - ) +""") scene.write(cls.mid_scene) - scene = GDScene.parse( - """ + scene = GDScene.parse(""" [gd_scene load_steps=2 format=2] [ext_resource path="res://Mid.tscn" type="PackedScene" id=1] [sub_resource type="CircleShape2D" id=1] @@ -160,8 +155,7 @@ def setUpClass(cls): shape = SubResource( 1 ) [node name="Sprite" type="Sprite" parent="." index="1"] flip_h = true -""" - ) +""") scene.write(cls.leaf_scene) @classmethod