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 diff --git a/godot_parser/files.py b/godot_parser/files.py index adf5e73..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_string(contents, parseAll=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 42d5004..0170cb6 100644 --- a/godot_parser/objects.py +++ b/godot_parser/objects.py @@ -13,6 +13,9 @@ "NodePath", "ExtResource", "SubResource", + "StringName", + "TypedArray", + "TypedDictionary", ] GD_OBJECT_REGISTRY = {} @@ -55,7 +58,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]), ) @@ -71,6 +74,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: @@ -245,3 +251,114 @@ def id(self) -> int: def id(self, id: int) -> None: """Setter for id""" 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" + 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 + + @classmethod + def from_parser(cls: Type["StringName"], parse_result) -> "StringName": + return StringName(parse_result[0]) + + def __str__(self) -> str: + return "&" + stringify_object(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) + + def __hash__(self): + return hash(self.str) diff --git a/godot_parser/sections.py b/godot_parser/sections.py index 7f22f5a..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, 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 42b1433..5728969 100644 --- a/godot_parser/util.py +++ b/godot_parser/util.py @@ -14,15 +14,20 @@ 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( - ['"%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}" ) 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 1b43031..ab81265 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, TypedArray, TypedDictionary boolean = ( (Keyword("true") | Keyword("false")) @@ -24,20 +24,27 @@ null = Keyword("null").set_parse_action(lambda _: [None]) +_string = QuotedString('"', escChar="\\", multiline=True).set_name("string") -primitive = ( - null | QuotedString('"', escChar="\\", multiline=True) | boolean | common.number +_string_name = ( + (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 ) -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( @@ -46,7 +53,17 @@ .set_name("list") .set_parse_action(lambda p: p.as_list()) ) -key_val = Group(QuotedString('"', escChar="\\") + Suppress(":") + value) + +# 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) # { # "_edit_use_anchors_": false @@ -57,6 +74,23 @@ .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") + + ( + 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_ | typed_list | dict_ | typed_dict | obj_ | primitive diff --git a/tests/test_gdfile.py b/tests/test_gdfile.py index 7706e0f..8715664 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=2] [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 89598dc..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 +from godot_parser import ( + Color, + ExtResource, + GDObject, + NodePath, + StringName, + SubResource, + TypedArray, + TypedDictionary, + Vector2, + Vector3, +) class TestGDObjects(unittest.TestCase): @@ -13,7 +24,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 +43,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 +68,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 +100,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 +108,53 @@ 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\\""') + + def test_typed_dictionary(self): + """Test for TypedDictionary""" + 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") +})""", + ) + + 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 975166f..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, parse +from godot_parser import ( + GDFile, + GDObject, + GDSection, + GDSectionHeader, + StringName, + TypedArray, + TypedDictionary, + Vector2, + parse, +) HERE = os.path.dirname(__file__) @@ -16,6 +26,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( @@ -110,6 +124,97 @@ ) ), ), + ( + """[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_single_quote": "'String'", + "string_name_single_quote": StringName("'StringName'"), + } + ) + ), + ), + ( + """[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")}, + ), + } + ) + ), + ), + ( + """[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\\\\" + """, + GDFile( + GDSection( + 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