Skip to content

Commit 5f70683

Browse files
Merge pull request #551 from cute-omega/master
Add a xml2remi module & command and using uv to manage environments
2 parents 18505f6 + 9a717cc commit 5f70683

10 files changed

Lines changed: 376 additions & 21 deletions

File tree

.python-version

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
3.12

pyproject.toml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
[project]
2+
authors = [
3+
{name = "Davide Rosa", email = "dddomodossola@gmail.com"},
4+
]
5+
dependencies = [
6+
"lxml>=6.0.2",
7+
"setuptools<81",
8+
]
9+
description = "Add your description here"
10+
keywords = ["gui-library", "remi", "platform-independent", "ui", "gui"]
11+
license = {file = "LICENSE"}
12+
name = "remi"
13+
readme = "README.md"
14+
requires-python = ">=3.8,<3.13"
15+
version = "2025.11.29"
16+
17+
[project.scripts]
18+
xml2remi = "xml2remi.__main__:main"
19+
20+
[build-system]
21+
build-backend = "setuptools.build_meta"
22+
requires = ["setuptools", "wheel"]

remi/__init__.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,11 @@
3131
)
3232

3333
from .server import App, Server, start
34-
from pkg_resources import get_distribution, DistributionNotFound
3534

3635
try:
37-
__version__ = get_distribution(__name__).version
38-
except DistributionNotFound:
39-
# package is not installed
36+
from importlib.metadata import version
37+
38+
__version__ = version(__name__)
39+
except ImportError:
40+
# Fallback for older Python versions
4041
pass

setup.py

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7,25 +7,26 @@
77
long_description = fh.read()
88

99
params = {
10-
'name':"remi",
11-
'description':"Python REMote Interface library",
12-
'use_scm_version':{'version_scheme': 'post-release'},
13-
'long_description':long_description,
14-
'long_description_content_type':"text/markdown",
15-
'url':"https://github.com/rawpython/remi",
16-
'download_url':"https://github.com/rawpython/remi/archive/master.zip",
17-
'keywords':["gui-library", "remi", "platform-independent", "ui", "gui"],
18-
'author':"Davide Rosa",
19-
'author_email':"dddomodossola@gmail.com",
20-
'license':"Apache",
21-
'packages':setuptools.find_packages(),
22-
'include_package_data':True,
23-
'setup_requires':['setuptools_scm'],
10+
"name": "remi",
11+
"description": "Python REMote Interface library",
12+
"use_scm_version": {"version_scheme": "post-release"},
13+
"long_description": long_description,
14+
"long_description_content_type": "text/markdown",
15+
"url": "https://github.com/rawpython/remi",
16+
"download_url": "https://github.com/rawpython/remi/archive/master.zip",
17+
"keywords": ["gui-library", "remi", "platform-independent", "ui", "gui"],
18+
"author": "Davide Rosa",
19+
"author_email": "dddomodossola@gmail.com",
20+
"license": "Apache",
21+
"packages": setuptools.find_packages(),
22+
"include_package_data": True,
23+
"setup_requires": ["setuptools_scm"],
24+
"entry_points": {"console_scripts": ["xml2remi = xml2remi"]},
2425
}
2526
try:
2627
setup(**params)
2728
except:
28-
del params['setup_requires']
29-
params['use_scm_version'] = False
30-
params['version'] = '2022.7.27'
29+
del params["setup_requires"]
30+
params["use_scm_version"] = False
31+
params["version"] = "2025.11.29"
3132
setup(**params)

uv.lock

Lines changed: 140 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

xml2remi/__init__.py

Whitespace-only changes.

xml2remi/__main__.py

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
import xml.etree.ElementTree as ET
2+
import sys, os
3+
import remi.gui as widget_list
4+
5+
try:
6+
import lxml.etree as lxml_ET
7+
8+
USE_LXML = True
9+
except ImportError:
10+
USE_LXML = False
11+
12+
13+
class RemiXMLTranslator:
14+
def __init__(self):
15+
# No longer need widget_map - we'll use dynamic class resolution
16+
pass
17+
18+
def translate_xml_to_code_from_root(self, root, file_path=None):
19+
"""Translate XML root element to Remi Python code."""
20+
code_lines = []
21+
code_lines.append("from remi import server, gui")
22+
code_lines.append("")
23+
24+
# Generate code for root widget
25+
root_var = self._generate_widget_code(root, code_lines, "root", file_path)
26+
code_lines.append("") # Add empty line after root initialization
27+
28+
# Add return statement
29+
code_lines.append(f"return {root_var}")
30+
31+
return "\n".join(code_lines)
32+
33+
def _generate_widget_code(
34+
self, element, code_lines: list[str], var_name: str, file_path: str = None
35+
):
36+
"""Recursively generate code for a widget and its children."""
37+
tag = element.tag
38+
if not hasattr(widget_list, tag):
39+
line_info = ""
40+
if USE_LXML and hasattr(element, "sourceline"):
41+
line_info = (
42+
f" at {os.path.abspath(file_path)}, line {element.sourceline}"
43+
)
44+
elif file_path:
45+
line_info = f" in {os.path.abspath(file_path)}"
46+
raise ValueError(f'Unknown widget type: "{tag}"{line_info}')
47+
widget_class = tag
48+
49+
# Collect attributes
50+
kwargs = {}
51+
for attr, value in element.attrib.items():
52+
match attr:
53+
case "width" | "height":
54+
kwargs[attr] = value
55+
case _:
56+
kwargs[attr] = f'"{value}"'
57+
58+
# Generate instantiation
59+
args_str = ", ".join([f"{k}={v}" for k, v in kwargs.items()])
60+
code_lines.append(f"{var_name} = gui.{widget_class}({args_str})")
61+
62+
# Generate code for children
63+
child_vars = []
64+
for i, child in enumerate(element):
65+
child_var = f"{var_name}_{child.tag}_{i}"
66+
child_vars.append(child_var)
67+
self._generate_widget_code(child, code_lines, child_var, file_path)
68+
key = child.attrib.get("key", "")
69+
if key:
70+
code_lines.append(f"{var_name}.append({child_var}, '{key}')")
71+
else:
72+
code_lines.append(f"{var_name}.append({child_var})")
73+
# Add empty line after append/add_tab, except for the last child
74+
if i < len(element) - 1:
75+
code_lines.append(
76+
""
77+
) # Add empty line for readability after append/add_tab
78+
79+
return var_name
80+
81+
def translate_xml_file_to_code(self, xml_file_path: str):
82+
"""Translate XML file to Remi Python code."""
83+
if USE_LXML:
84+
tree = lxml_ET.parse(xml_file_path)
85+
root = tree.getroot()
86+
else:
87+
with open(xml_file_path, "r", encoding="utf-8") as f:
88+
xml_string = f.read()
89+
root = ET.fromstring(xml_string)
90+
return self.translate_xml_to_code_from_root(root, xml_file_path)
91+
92+
93+
# Command line usage
94+
def main():
95+
if len(sys.argv) != 2:
96+
print("Usage: python translator.py <xml_file>")
97+
sys.exit(1)
98+
99+
xml_file = sys.argv[1]
100+
translator = RemiXMLTranslator()
101+
code = translator.translate_xml_file_to_code(xml_file)
102+
103+
# Parse the generated code
104+
lines = code.split("\n")
105+
import_line = lines[0]
106+
code_lines = lines[2:-1] # Remove import, empty line, and return statement
107+
ui_code = "\n".join(
108+
" " + line if line.strip() else "" for line in code_lines
109+
)
110+
111+
base_name = os.path.splitext(xml_file)[0]
112+
py_file = base_name + ".py"
113+
114+
full_code = f"""{import_line}
115+
116+
class MyApp(server.App):
117+
def __init__(self, *args):
118+
super(MyApp, self).__init__(*args)
119+
120+
def main(self):
121+
{ui_code}
122+
# add your code here
123+
124+
return root
125+
126+
if __name__ == "__main__":
127+
server.start(MyApp)
128+
"""
129+
130+
with open(py_file, "w", encoding="utf-8") as f:
131+
f.write(full_code)
132+
133+
print(f"Generated {py_file}")
134+
135+
136+
if __name__ == "__main__":
137+
main()

xml2remi/error_example.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<VBox>
2+
<Label text="Hello World" />
3+
<WrongWidget text="Error" />
4+
<Button text="Click Me" />
5+
</VBox>

xml2remi/example.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
from remi import server, gui
2+
3+
class MyApp(server.App):
4+
def __init__(self, *args):
5+
super(MyApp, self).__init__(*args)
6+
7+
def main(self):
8+
root = gui.VBox()
9+
root_Label_0 = gui.Label(text="Hello World")
10+
root.append(root_Label_0)
11+
12+
root_Button_1 = gui.Button(text="Click Me")
13+
root.append(root_Button_1)
14+
15+
root_TabBox_2 = gui.TabBox()
16+
root_TabBox_2_VBox_0 = gui.VBox()
17+
root_TabBox_2_VBox_0_Label_0 = gui.Label(text="Content of Tab 1")
18+
root_TabBox_2_VBox_0.append(root_TabBox_2_VBox_0_Label_0)
19+
root_TabBox_2.append(root_TabBox_2_VBox_0)
20+
21+
root_TabBox_2_HBox_1 = gui.HBox()
22+
root_TabBox_2_HBox_1_TextInput_0 = gui.TextInput()
23+
root_TabBox_2_HBox_1.append(root_TabBox_2_HBox_1_TextInput_0)
24+
25+
root_TabBox_2_HBox_1_CheckBox_1 = gui.CheckBox()
26+
root_TabBox_2_HBox_1.append(root_TabBox_2_HBox_1_CheckBox_1)
27+
root_TabBox_2.append(root_TabBox_2_HBox_1)
28+
root.append(root_TabBox_2)
29+
30+
# add your code here
31+
32+
return root
33+
34+
if __name__ == "__main__":
35+
server.start(MyApp)

xml2remi/example.xml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<VBox>
2+
<Label text="Hello World" />
3+
<Button text="Click Me" />
4+
<TabBox>
5+
<VBox>
6+
<Label text="Content of Tab 1" />
7+
</VBox>
8+
<HBox>
9+
<TextInput />
10+
<CheckBox />
11+
</HBox>
12+
</TabBox>
13+
</VBox>

0 commit comments

Comments
 (0)