dearpygui-forms 0.1.2__tar.gz → 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -10,5 +10,6 @@ wheels/
10
10
  .venv
11
11
 
12
12
  # Project-specific files
13
+ .env
13
14
  data/
14
15
  *.ini
@@ -0,0 +1 @@
1
+ 3.10
@@ -0,0 +1,64 @@
1
+ Metadata-Version: 2.4
2
+ Name: dearpygui-forms
3
+ Version: 0.2.0
4
+ Summary: Dearpygui extention for autogeneration forms powered by pydantic.
5
+ Project-URL: homepage, https://github.com/vlewicki/dearpygui-forms
6
+ Author-email: Valentin Lewicki <vlewicki@vlewicki.ru>
7
+ Requires-Python: >=3.10
8
+ Requires-Dist: dearpygui>=2.0.0
9
+ Requires-Dist: loguru>=0.7.3
10
+ Requires-Dist: pydantic>=2.11.7
11
+ Description-Content-Type: text/markdown
12
+
13
+ # DearPyGui Forms
14
+ Generate GUI forms for your Pydantic models.
15
+
16
+ ## Features
17
+ - Fill form fields with Pydantic model data
18
+ - Fill from fields with default values
19
+ - Validation on form submission
20
+ - Callbacks for form submission
21
+
22
+
23
+ Currently supported Pydantic fields:
24
+ - str
25
+ - int
26
+ - float
27
+ - bool
28
+ - datetime
29
+ - date
30
+ - time
31
+
32
+ ## Example:
33
+ ```python
34
+ from pprint import pprint
35
+ import dearpygui.dearpygui as dpg
36
+ from pydantic import BaseModel, Field
37
+ from dearpygui_forms import DPGForm
38
+
39
+ class User(BaseModel):
40
+ name: str = Field(default="John Doe", min_length=3)
41
+ age: int = Field(ge=18)
42
+
43
+
44
+ class Storage(BaseModel):
45
+ users: list[User] = []
46
+
47
+ class UserForm(DPGForm, model=User):
48
+ pass
49
+
50
+
51
+ dpg.create_context()
52
+ dpg.create_viewport()
53
+
54
+ store = Storage()
55
+
56
+ with dpg.window(label="User Form"):
57
+ user_form = UserForm(callback=lambda x: store.users.append(x))
58
+ user_form.add()
59
+ dpg.add_button(label="Print Users", callback=lambda: pprint(store.model_dump()))
60
+ dpg.setup_dearpygui()
61
+ dpg.show_viewport()
62
+ dpg.start_dearpygui()
63
+ dpg.destroy_context()
64
+ ```
@@ -0,0 +1,52 @@
1
+ # DearPyGui Forms
2
+ Generate GUI forms for your Pydantic models.
3
+
4
+ ## Features
5
+ - Fill form fields with Pydantic model data
6
+ - Fill from fields with default values
7
+ - Validation on form submission
8
+ - Callbacks for form submission
9
+
10
+
11
+ Currently supported Pydantic fields:
12
+ - str
13
+ - int
14
+ - float
15
+ - bool
16
+ - datetime
17
+ - date
18
+ - time
19
+
20
+ ## Example:
21
+ ```python
22
+ from pprint import pprint
23
+ import dearpygui.dearpygui as dpg
24
+ from pydantic import BaseModel, Field
25
+ from dearpygui_forms import DPGForm
26
+
27
+ class User(BaseModel):
28
+ name: str = Field(default="John Doe", min_length=3)
29
+ age: int = Field(ge=18)
30
+
31
+
32
+ class Storage(BaseModel):
33
+ users: list[User] = []
34
+
35
+ class UserForm(DPGForm, model=User):
36
+ pass
37
+
38
+
39
+ dpg.create_context()
40
+ dpg.create_viewport()
41
+
42
+ store = Storage()
43
+
44
+ with dpg.window(label="User Form"):
45
+ user_form = UserForm(callback=lambda x: store.users.append(x))
46
+ user_form.add()
47
+ dpg.add_button(label="Print Users", callback=lambda: pprint(store.model_dump()))
48
+ dpg.setup_dearpygui()
49
+ dpg.show_viewport()
50
+ dpg.start_dearpygui()
51
+ dpg.destroy_context()
52
+ ```
@@ -1,20 +1,20 @@
1
1
  [project]
2
2
  name = "dearpygui-forms"
3
- version = "0.1.2"
3
+ version = "0.2.0"
4
4
  description = "Dearpygui extention for autogeneration forms powered by pydantic."
5
5
  readme = "README.md"
6
6
  authors = [
7
7
  { name = "Valentin Lewicki", email = "vlewicki@vlewicki.ru" }
8
8
  ]
9
- requires-python = ">=3.13"
9
+ requires-python = ">=3.10"
10
10
  dependencies = [
11
11
  "dearpygui>=2.0.0",
12
12
  "loguru>=0.7.3",
13
13
  "pydantic>=2.11.7",
14
14
  ]
15
15
 
16
- [project.scripts]
17
- dearpygui-forms = "dearpygui_forms:main"
16
+ [project.urls]
17
+ homepage = "https://github.com/vlewicki/dearpygui-forms"
18
18
 
19
19
 
20
20
  [build-system]
@@ -25,6 +25,3 @@ build-backend = "hatchling.build"
25
25
  dev = [
26
26
  "pylint>=3.3.7",
27
27
  ]
28
-
29
- [tools.hatch.build]
30
- include = ["src", "README.md"]
@@ -0,0 +1,4 @@
1
+ """
2
+ Dearpygui extention for autogeneration forms powered by pydantic.
3
+ """
4
+ from .dpg_form import DPGForm
@@ -0,0 +1,53 @@
1
+ from pprint import pformat
2
+ from typing import Any, Type
3
+ import copy
4
+
5
+ import pydantic
6
+ import dearpygui.dearpygui as dpg
7
+ from loguru import logger
8
+
9
+
10
+ from .models import PropertySchema
11
+ from .widgets import *
12
+
13
+
14
+ def extract_defs(schema: dict[str, Any]) -> tuple[dict[str, Any], dict[str, Any]]:
15
+ """split schema on root models schema and $defs"""
16
+ schema = copy.deepcopy(schema)
17
+ if "$defs" in schema:
18
+ defs = schema["$defs"]
19
+ del schema["$defs"]
20
+ root_model_schema = schema
21
+ else:
22
+ defs = {}
23
+ root_model_schema = schema
24
+ return root_model_schema, defs
25
+
26
+
27
+ class DPGForm:
28
+ def __init_subclass__(cls, *, model: Type[pydantic.BaseModel]) -> None:
29
+ cls._Model = model
30
+
31
+ def __init__(self, callback):
32
+ model_schema, models_defs = extract_defs(self._Model.model_json_schema())
33
+ self._model_widget = generate_widget(model_schema, models_defs)
34
+ self._callback = callback
35
+
36
+ def add(self):
37
+ self._model_widget.add()
38
+ dpg.add_button(label="Submit", callback=self._on_submit)
39
+
40
+ def fill_form(self, data_model: pydantic.BaseModel):
41
+ self._model_widget.set_value(data_model.model_dump())
42
+
43
+ def _on_submit(self):
44
+ try:
45
+ data = self._Model(**self._model_widget.get_value())
46
+ except pydantic.ValidationError as e:
47
+ with dpg.window(modal=True, label="Validation Error"):
48
+ dpg.add_text(f"Validation error: {e}")
49
+ else:
50
+ self._callback(data)
51
+
52
+ def get_form_data(self):
53
+ return self._Model(**self._model_widget.get_value())
@@ -0,0 +1,14 @@
1
+ from typing import Any, Literal
2
+ from pydantic import BaseModel
3
+
4
+
5
+ class PropertySchema:
6
+ def __init__(self, schema: dict[str, Any]) -> None:
7
+ self.title: str | None = schema.get("title", None)
8
+ self.type: str | None = schema.get("type", None)
9
+ self.anyOf: list[dict[str, Any]] = schema.get("anyOf", [])
10
+ self.properties: dict[str, dict[str, Any]] = schema.get("properties", {})
11
+ self.default = schema.get("default", None)
12
+
13
+ def __repr__(self) -> str:
14
+ return f"PropertySchema(title={self.title}, type={self.type}, anyOf={self.anyOf}, properties={self.properties}, default={self.default})"
@@ -0,0 +1,216 @@
1
+ import copy
2
+ from typing import Any
3
+
4
+ import dearpygui.dearpygui as dpg
5
+ from loguru import logger
6
+
7
+
8
+ from .models import PropertySchema
9
+
10
+
11
+ @logger.catch
12
+ def dereference_property(schema: dict[str, Any], defs: dict[str, Any]):
13
+ if "$ref" in schema:
14
+ path = schema["$ref"].split("/")[-1]
15
+ new_schema = copy.deepcopy(defs[path])
16
+ new_schema.update(copy.deepcopy(schema))
17
+ del new_schema["$ref"]
18
+ else:
19
+ new_schema = copy.deepcopy(schema)
20
+ return new_schema
21
+
22
+
23
+ class Widget:
24
+ def __init__(self, schema: PropertySchema, defs: dict, **kwargs):
25
+ self.schema = schema
26
+ self._defs = defs
27
+ self._kwargs = kwargs
28
+ with dpg.stage() as self._staging_container_id:
29
+ self._ui()
30
+
31
+ if schema.default:
32
+ self.set_value(schema.default)
33
+
34
+ def _ui(self):
35
+ dpg.add_text(f"Property {self.schema.title}, type: {self.schema.type}")
36
+
37
+ def add(self):
38
+ dpg.unstage(self._staging_container_id)
39
+
40
+ def set_value(self, value: Any):
41
+ pass
42
+
43
+ def get_value(self) -> Any:
44
+ pass
45
+
46
+
47
+
48
+ class ObjectWidget(Widget):
49
+ def __init__(self, schema, defs, **kwargs):
50
+ self._properties = {}
51
+ super().__init__(schema, defs, **kwargs)
52
+
53
+ def _ui(self):
54
+ for property_name, property in self.schema.properties.items():
55
+ property_widget = generate_widget(property, self._defs, generate_object=False)
56
+ property_widget.add()
57
+ self._properties[property_name] = property_widget
58
+
59
+ def get_value(self) -> dict[str, Any]:
60
+ return {property_name: property_widget.get_value() for property_name, property_widget in self._properties.items()}
61
+
62
+ def set_value(self, value: dict[str, Any]):
63
+ for property_name, property_widget in self._properties.items():
64
+ property_widget.set_value(value.get(property_name))
65
+
66
+
67
+ class ArrayWidget(Widget):
68
+ def __init__(self, schema, defs, **kwargs):
69
+ super().__init__(schema, defs, **kwargs)
70
+
71
+
72
+ class MultiTypeWidget(Widget):
73
+ def __init__(self, schema, defs, **kwargs):
74
+ self._type_switcher_id = dpg.generate_uuid()
75
+ self._widget: Widget | None = None
76
+ self._widgets = [generate_widget(type_schema, defs) for type_schema in schema.anyOf]
77
+ super().__init__(schema, defs, **kwargs)
78
+ logger.debug(defs)
79
+
80
+
81
+ def _ui(self):
82
+ dpg.add_text(self.schema.title)
83
+ with dpg.group(indent=10):
84
+ dpg.add_combo(label="Type", tag=self._type_switcher_id, items=tuple(x.schema.title or x.schema.type for x in self._widgets), callback=self.switch_value_type)
85
+
86
+
87
+ def switch_value_type(self, new_type):
88
+ logger.debug(f"Switching to type {dpg.get_value(self._type_switcher_id)}")
89
+
90
+
91
+ def get_value(self):
92
+ if self._widget:
93
+ return self._widget.get_value()
94
+ else:
95
+ return None
96
+
97
+ def set_value(self, value):
98
+ dpg.set_value(self._type_switcher_id, value)
99
+
100
+
101
+ class StringWidget(Widget):
102
+ def __init__(self, schema, defs, **kwargs):
103
+ self._input_id = dpg.generate_uuid()
104
+ super().__init__(schema, defs, **kwargs)
105
+
106
+ def _ui(self):
107
+ dpg.add_input_text(label=self.schema.title, tag=self._input_id)
108
+
109
+ def get_value(self):
110
+ return dpg.get_value(self._input_id)
111
+
112
+ def set_value(self, value):
113
+ dpg.set_value(self._input_id, value)
114
+
115
+
116
+ class IntegerWidget(Widget):
117
+ def __init__(self, schema, defs, **kwargs):
118
+ self._input_id = dpg.generate_uuid()
119
+ super().__init__(schema, defs, **kwargs)
120
+
121
+ def _ui(self):
122
+ dpg.add_input_int(label=self.schema.title, tag=self._input_id)
123
+
124
+ def get_value(self):
125
+ return dpg.get_value(self._input_id)
126
+
127
+ def set_value(self, value):
128
+ dpg.set_value(self._input_id, value)
129
+
130
+
131
+ class NumberWidget(Widget):
132
+ def __init__(self, schema, defs, **kwargs):
133
+ self._input_id = dpg.generate_uuid()
134
+ super().__init__(schema, defs, **kwargs)
135
+
136
+ def _ui(self):
137
+ dpg.add_input_float(label=self.schema.title, tag=self._input_id)
138
+
139
+ def get_value(self):
140
+ return dpg.get_value(self._input_id)
141
+
142
+ def set_value(self, value):
143
+ dpg.set_value(self._input_id, value)
144
+
145
+
146
+ class BooleanWidget(Widget):
147
+ def __init__(self, schema, defs, **kwargs):
148
+ self._checkbox_id = dpg.generate_uuid()
149
+ super().__init__(schema, defs, **kwargs)
150
+
151
+ def _ui(self):
152
+ dpg.add_checkbox(label=self.schema.title, tag=self._checkbox_id)
153
+
154
+ def get_value(self):
155
+ return dpg.get_value(self._checkbox_id)
156
+
157
+ def set_value(self, value: bool):
158
+ dpg.set_value(self._checkbox_id, value)
159
+
160
+ class NoneWidget(Widget):
161
+ def __init__(self, schema, defs, **kwargs):
162
+ self._none_id = dpg.generate_uuid()
163
+ super().__init__(schema, defs, **kwargs)
164
+
165
+ def _ui(self):
166
+ dpg.add_input_text(default_value='<None>', label=self.schema.title, tag=self._none_id, enabled=False)
167
+
168
+ def get_value(self):
169
+ return None
170
+
171
+ def set_value(self, value):
172
+ pass
173
+
174
+
175
+ class ExternalWidget(Widget):
176
+ def __init__(self, schema, defs, **kwargs):
177
+ self._external_id = dpg.generate_uuid()
178
+ super().__init__(schema, defs, **kwargs)
179
+
180
+ def _ui(self):
181
+ dpg.add_button(label=self.schema.title, tag=self._external_id, enabled=False)
182
+
183
+ def get_value(self):
184
+ return None
185
+
186
+ def set_value(self, value):
187
+ pass
188
+
189
+
190
+ def generate_widget(json_schema: dict[str, Any], defs: dict[str, Any], generate_object: bool = True, **kwargs) -> Widget:
191
+ schema = PropertySchema(dereference_property(json_schema, defs))
192
+ match schema:
193
+ case PropertySchema(type='object'):
194
+ if generate_object:
195
+ return ObjectWidget(schema, defs, **kwargs)
196
+ else:
197
+ raise NotImplementedError("ExternalWidget is not implemented yet")
198
+ return ExternalWidget(schema, defs, **kwargs)
199
+ case PropertySchema(type='array'):
200
+ raise NotImplementedError("ArrayWidget is not implemented yet")
201
+ return ArrayWidget(schema, defs, **kwargs)
202
+ case PropertySchema(type='string'):
203
+ return StringWidget(schema, defs, **kwargs)
204
+ case PropertySchema(type='integer'):
205
+ return IntegerWidget(schema, defs, **kwargs)
206
+ case PropertySchema(type='number'):
207
+ return NumberWidget(schema, defs, **kwargs)
208
+ case PropertySchema(type='boolean'):
209
+ return BooleanWidget(schema, defs, **kwargs)
210
+ case PropertySchema(type='null'):
211
+ return NoneWidget(schema, defs, **kwargs)
212
+ case PropertySchema(anyOf=types) if len(types) > 0:
213
+ raise NotImplementedError("MultiTypeWidget is not implemented yet")
214
+ # return MultiTypeWidget(schema, defs, **kwargs)
215
+ case _:
216
+ raise ValueError(f"Unsupported schema: {schema}")
@@ -1,8 +1,10 @@
1
1
  import datetime
2
2
  import decimal
3
+ from pprint import pprint
3
4
  import dearpygui.dearpygui as dpg
4
5
 
5
6
  import dearpygui.dearpygui as dpg
7
+ from loguru import logger
6
8
  from pydantic import BaseModel, Field
7
9
 
8
10
  from dearpygui import demo
@@ -10,6 +12,11 @@ import pydantic
10
12
  from dearpygui_forms import DPGForm
11
13
 
12
14
 
15
+ class Tool(BaseModel):
16
+ name: str = Field(title="Tool name", min_length=1)
17
+ price: float = Field(title="Price", ge=0)
18
+ quantity: int = Field(title="Quantity", ge=0)
19
+
13
20
  class User(BaseModel):
14
21
  name: str = Field(title="User name", min_length=1)
15
22
  birthday: datetime.date = datetime.date(2000, 1, 1)
@@ -19,13 +26,17 @@ class User(BaseModel):
19
26
  weight: float = 60.0
20
27
  pi: decimal.Decimal = decimal.Decimal('3.1415')
21
28
  male: bool = True
29
+ tool: Tool | int
30
+ best_friend: 'User' = Field(title='Best friend')
31
+ zero: None
22
32
  # friends: list['User']
23
33
 
24
34
 
35
+ class UserForm(DPGForm, model=User):
36
+ pass
25
37
 
26
- class UserForm(DPGForm):
27
- __pydantic_model__ = User
28
-
38
+ class ToolForm(DPGForm, model=Tool):
39
+ pass
29
40
 
30
41
 
31
42
  def main():
@@ -52,11 +63,11 @@ def main():
52
63
  dpg.add_menu_item(label='About', callback=dpg.show_about)
53
64
 
54
65
  with dpg.window(label="Template", width=641, height=480):
55
- dpg.add_text("Hello")
56
-
57
- with dpg.window(label="Main"):
58
- dpg.add_text("Hello")
59
- UserForm(print).add()
66
+ UserForm(lambda x: logger.success(x)).add()
67
+ tf = ToolForm(lambda x: logger.success(x))
68
+ tf.add()
69
+ t = Tool(name='Tool', price=100.55, quantity=22)
70
+ tf.fill_form(t)
60
71
 
61
72
 
62
73
 
@@ -0,0 +1,30 @@
1
+ from pprint import pprint
2
+ import dearpygui.dearpygui as dpg
3
+ from pydantic import BaseModel, Field
4
+ from dearpygui_forms import DPGForm
5
+
6
+ class User(BaseModel):
7
+ name: str = Field(default="John Doe", min_length=3)
8
+ age: int = Field(ge=18)
9
+
10
+
11
+ class Storage(BaseModel):
12
+ users: list[User] = []
13
+
14
+ class UserForm(DPGForm, model=User):
15
+ pass
16
+
17
+
18
+ dpg.create_context()
19
+ dpg.create_viewport()
20
+
21
+ store = Storage()
22
+
23
+ with dpg.window(label="User Form"):
24
+ user_form = UserForm(callback=lambda x: store.users.append(x))
25
+ user_form.add()
26
+ dpg.add_button(label="Print Users", callback=lambda: pprint(store.model_dump()))
27
+ dpg.setup_dearpygui()
28
+ dpg.show_viewport()
29
+ dpg.start_dearpygui()
30
+ dpg.destroy_context()
@@ -0,0 +1,37 @@
1
+ import enum
2
+ from pprint import pprint
3
+ from pydantic import BaseModel
4
+
5
+
6
+ models: list[type[BaseModel]] = []
7
+
8
+
9
+ class Category(enum.Enum):
10
+ FOOD = "food"
11
+ ENTERTAINMENT = "entertainment"
12
+ TRANSPORTATION = "transportation"
13
+ HOUSING = "housing"
14
+ UTILITIES = "utilities"
15
+ OTHER = 0
16
+
17
+ class Transaction(BaseModel):
18
+ category: Category
19
+
20
+
21
+ models.append(Transaction)
22
+
23
+ class Test1(BaseModel):
24
+ val: int | None = None
25
+
26
+ models.append(Test1)
27
+
28
+
29
+ class Test2(BaseModel):
30
+ val: Transaction | Test1
31
+
32
+ models.append(Test2)
33
+
34
+
35
+ for model in models:
36
+ pprint(model.model_json_schema())
37
+ print(Test1().model_dump_json())