Skip to content

Commit

Permalink
add initial generic section value support (#5)
Browse files Browse the repository at this point in the history
  • Loading branch information
YajJackson authored Apr 14, 2024
1 parent 3da8205 commit a02250b
Show file tree
Hide file tree
Showing 8 changed files with 247 additions and 28 deletions.
3 changes: 2 additions & 1 deletion godot_parser/files.py
Original file line number Diff line number Diff line change
Expand Up @@ -302,7 +302,8 @@ def get_node(self, path: str = ".") -> Optional[GDNodeSection]:
@classmethod
def parse(cls, contents: str):
"""Parse the contents of a Godot file"""
return cls.from_parser(scene_file.parse_string(contents, parseAll=True))
parsed_scene = scene_file.parse_string(contents, parseAll=True)
return cls.from_parser(parsed_scene)

@classmethod
def load(cls, filepath: str):
Expand Down
11 changes: 6 additions & 5 deletions godot_parser/objects.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
""" Wrappers for Godot's non-primitive object types """

from functools import partial
from typing import Type, TypeVar
from typing import TypeVar

from .util import stringify_object

Expand Down Expand Up @@ -49,10 +49,11 @@ def __init__(self, name, *args) -> None:
self.args = list(args)

@classmethod
def from_parser(cls: Type[GDObjectType], parse_result) -> GDObjectType:
name = parse_result[0]
factory = GD_OBJECT_REGISTRY.get(name, partial(GDObject, name))
return factory(*parse_result[1:])
def from_parser(cls, parse_result):
type_name = parse_result[0]
args = parse_result[1:] if len(parse_result) > 1 else []
factory = GD_OBJECT_REGISTRY.get(type_name, partial(GDObject, type_name))
return factory(*args)

def __str__(self) -> str:
return "%s( %s )" % (
Expand Down
21 changes: 20 additions & 1 deletion godot_parser/sections.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from .objects import ExtResource, SubResource
from .util import stringify_object
from .values import value

__all__ = [
"GDSectionHeader",
Expand Down Expand Up @@ -129,15 +130,33 @@ def from_parser(cls: Type[GDSectionType], parse_result) -> GDSectionType:
section.header = header
section.properties = OrderedDict()
for k, v in parse_result[1:]:
if isinstance(v, tuple):
# Handle generic types like ('Array[int]', [1, 5, 3])
section[k] = v[1]
section[k] = v
return section

@staticmethod
def format_value(value: Any):
"""Formats the value based on its type, specifically handling generic type tuples."""
if (
isinstance(value, tuple)
and len(value) == 2
and isinstance(value[0], str)
and isinstance(value[1], list)
):
# Handle generic types like ('Array[int]', [1, 5, 3])
type_name, elements = value
return f"{type_name}({elements})"
else:
return stringify_object(value)

def __str__(self) -> str:
ret = str(self.header)
if self.properties:
ret += "\n" + "\n".join(
[
"%s = %s" % (k, stringify_object(v))
"%s = %s" % (k, self.format_value(v))
for k, v in self.properties.items()
]
)
Expand Down
22 changes: 20 additions & 2 deletions godot_parser/values.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ def parse_action_function(parse_results: ParseResults) -> bool:
null | QuotedString('"', escChar="\\", multiline=True) | boolean | common.number
)
value = Forward()
non_generic_types = Forward()

# Vector2( 1, 2 )
obj_type = (
Expand Down Expand Up @@ -69,6 +70,23 @@ def parse_action_function(parse_results: ParseResults) -> bool:
.set_parse_action(lambda d: {k: v for k, v in d})
)

# Exports
non_generic_types <<= primitive | list_ | dict_ | obj_type


# Handles constructs like Array[Object](...)
def parse_generic_type(parse_results: ParseResults):
toks = parse_results.asList()
type_name = toks[0]
args = toks[1:]
return (type_name, *args)


value <<= primitive | list_ | dict_ | obj_type
generic_type = (
Word(alphas, alphanums + "[]")
+ Suppress("(")
+ Opt(DelimitedList(value, delim=","))
+ Suppress(")")
).set_parse_action(parse_generic_type)

# Exports
value <<= non_generic_types | generic_type
25 changes: 6 additions & 19 deletions test_parse_files.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
import argparse
import os
import sys
from itertools import zip_longest

from godot_parser import load, parse

Expand All @@ -21,32 +20,20 @@ def _parse_and_test_file(filename: str) -> bool:
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
assert data is not None
assert f is not None

print(f"parsed file: {filename}")
# breakpoint()
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("debug", help="Parse file or files under this directory")
args = parser.parse_args()
if os.path.isfile(args.file_or_dir):
_parse_and_test_file(args.file_or_dir)
Expand Down
90 changes: 90 additions & 0 deletions tests/example_scenes/godot-demo-project-combat-info.tscn
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
[gd_scene load_steps=6 format=3 uid="uid://bypumcqt7j0iv"]

[ext_resource type="FontFile" uid="uid://b2r8e5d6rc4pg" path="res://theme/fonts/montserrat_extra_bold.otf" id="1"]

[sub_resource type="FontFile" id="1"]
fallbacks = Array[Font]([ExtResource("1")])
face_index = null
embolden = null
transform = null
cache/0/16/0/ascent = 0.0
cache/0/16/0/descent = 0.0
cache/0/16/0/underline_position = 0.0
cache/0/16/0/underline_thickness = 0.0
cache/0/16/0/scale = 1.0
cache/0/16/0/kerning_overrides/16/0 = Vector2(0, 0)

[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_dsedd"]
bg_color = Color(0.541176, 0.72549, 0.819608, 1)
border_width_left = 5
border_width_top = 5
border_width_right = 5
border_width_bottom = 5
border_color = Color(0.4488, 0.602933, 0.68, 1)
border_blend = true
corner_radius_top_left = 16
corner_radius_top_right = 16
corner_radius_bottom_right = 16
corner_radius_bottom_left = 16

[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_s3yjq"]
bg_color = Color(0.945098, 0.235294, 0.376471, 1)
border_width_left = 5
border_width_top = 5
border_width_right = 5
border_width_bottom = 5
border_color = Color(0.8, 0.8, 0.8, 0)
corner_radius_top_left = 16
corner_radius_top_right = 16
corner_radius_bottom_right = 16
corner_radius_bottom_left = 16

[sub_resource type="Theme" id="Theme_vywfd"]
ProgressBar/styles/background = SubResource("StyleBoxFlat_dsedd")
ProgressBar/styles/fill = SubResource("StyleBoxFlat_s3yjq")

[node name="Info" type="PanelContainer"]
offset_right = 400.0
offset_bottom = 239.0
scale = Vector2(0.907481, 1)
size_flags_horizontal = 3
size_flags_vertical = 3

[node name="VBoxContainer" type="VBoxContainer" parent="."]
custom_minimum_size = Vector2(0, 150)
layout_mode = 2
alignment = 1

[node name="NameContainer" type="CenterContainer" parent="VBoxContainer"]
layout_mode = 2
size_flags_vertical = 3

[node name="Name" type="Label" parent="VBoxContainer/NameContainer"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 7
theme_override_colors/font_color = Color(0.0745098, 0.27451, 0.368627, 1)
theme_override_colors/font_shadow_color = Color(0.184314, 0.419608, 0.533333, 0.356863)
theme_override_constants/shadow_offset_x = 1
theme_override_constants/shadow_offset_y = 2
theme_override_fonts/font = SubResource("1")
text = "{NAME}"
horizontal_alignment = 1
vertical_alignment = 1
uppercase = true

[node name="HealthContainer" type="VBoxContainer" parent="VBoxContainer"]
layout_mode = 2
size_flags_vertical = 3

[node name="Health" type="ProgressBar" parent="VBoxContainer/HealthContainer"]
custom_minimum_size = Vector2(300, 50)
layout_mode = 2
size_flags_horizontal = 4
size_flags_vertical = 2
theme = SubResource("Theme_vywfd")
max_value = 10.0
step = 1.0
value = 5.0
rounded = true
show_percentage = false
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
[gd_scene load_steps=6 format=3 uid="uid://bypumcqt7j0iv"]

[ext_resource type="FontFile" uid="uid://b2r8e5d6rc4pg" path="res://theme/fonts/montserrat_extra_bold.otf" id="1"]

[sub_resource type="FontFile" id="1"]
fallbacks = Array[Font]([ExtResource( "1" )])
face_index = null
embolden = null
transform = null
cache/0/16/0/ascent = 0.0
cache/0/16/0/descent = 0.0
cache/0/16/0/underline_position = 0.0
cache/0/16/0/underline_thickness = 0.0
cache/0/16/0/scale = 1.0
cache/0/16/0/kerning_overrides/16/0 = Vector2( 0, 0 )

[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_dsedd"]
bg_color = Color( 0.541176, 0.72549, 0.819608, 1 )
border_width_left = 5
border_width_top = 5
border_width_right = 5
border_width_bottom = 5
border_color = Color( 0.4488, 0.602933, 0.68, 1 )
border_blend = true
corner_radius_top_left = 16
corner_radius_top_right = 16
corner_radius_bottom_right = 16
corner_radius_bottom_left = 16

[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_s3yjq"]
bg_color = Color( 0.945098, 0.235294, 0.376471, 1 )
border_width_left = 5
border_width_top = 5
border_width_right = 5
border_width_bottom = 5
border_color = Color( 0.8, 0.8, 0.8, 0 )
corner_radius_top_left = 16
corner_radius_top_right = 16
corner_radius_bottom_right = 16
corner_radius_bottom_left = 16

[sub_resource type="Theme" id="Theme_vywfd"]
ProgressBar/styles/background = SubResource( "StyleBoxFlat_dsedd" )
ProgressBar/styles/fill = SubResource( "StyleBoxFlat_s3yjq" )

[node name="Info" type="PanelContainer"]
offset_right = 400.0
offset_bottom = 239.0
scale = Vector2( 0.907481, 1 )
size_flags_horizontal = 3
size_flags_vertical = 3

[node name="VBoxContainer" type="VBoxContainer" parent="."]
custom_minimum_size = Vector2( 0, 150 )
layout_mode = 2
alignment = 1

[node name="NameContainer" type="CenterContainer" parent="VBoxContainer"]
layout_mode = 2
size_flags_vertical = 3

[node name="Name" type="Label" parent="VBoxContainer/NameContainer"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 7
theme_override_colors/font_color = Color( 0.0745098, 0.27451, 0.368627, 1 )
theme_override_colors/font_shadow_color = Color( 0.184314, 0.419608, 0.533333, 0.356863 )
theme_override_constants/shadow_offset_x = 1
theme_override_constants/shadow_offset_y = 2
theme_override_fonts/font = SubResource( "1" )
text = "{NAME}"
horizontal_alignment = 1
vertical_alignment = 1
uppercase = true

[node name="HealthContainer" type="VBoxContainer" parent="VBoxContainer"]
layout_mode = 2
size_flags_vertical = 3

[node name="Health" type="ProgressBar" parent="VBoxContainer/HealthContainer"]
custom_minimum_size = Vector2( 300, 50 )
layout_mode = 2
size_flags_horizontal = 4
size_flags_vertical = 2
theme = SubResource( "Theme_vywfd" )
max_value = 10.0
step = 1.0
value = 5.0
rounded = true
show_percentage = false
13 changes: 13 additions & 0 deletions tests/test_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

from godot_parser import GDFile, GDObject, GDSection, GDSectionHeader, Vector2, parse
from godot_parser.files import GDFileType
from tests.snapshot_manager import SnapshotManager

HERE = os.path.dirname(__file__)

Expand Down Expand Up @@ -119,6 +120,9 @@


class TestParser(unittest.TestCase):

snapshot_manager = SnapshotManager()

def _run_test(self, string: str, expected: GDFileType):
"""Run a set of tests"""
parse_result: Optional[GDFileType] = None
Expand Down Expand Up @@ -147,3 +151,12 @@ def test_cases(self):
"""Run the parsing test cases"""
for string, expected in TEST_CASES:
self._run_test(string, expected)

def test_example_data(self):
examples = os.path.join(HERE, "example_scenes")
for file in os.listdir(examples):
print(f'Parsing {file}')
with open(os.path.join(examples, file), "r") as f:
content = f.read()
parsed = parse(content)
self.snapshot_manager.assert_match(str(parsed), f'{file}.parsed')

0 comments on commit a02250b

Please sign in to comment.