pygpt-net 2.6.3__py3-none-any.whl → 2.6.5__py3-none-any.whl

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.
Files changed (76) hide show
  1. pygpt_net/CHANGELOG.txt +10 -0
  2. pygpt_net/__init__.py +2 -2
  3. pygpt_net/config.py +55 -65
  4. pygpt_net/controller/__init__.py +5 -2
  5. pygpt_net/controller/chat/chat.py +38 -35
  6. pygpt_net/controller/chat/render.py +144 -217
  7. pygpt_net/controller/chat/stream.py +51 -25
  8. pygpt_net/controller/config/config.py +39 -42
  9. pygpt_net/controller/config/field/checkbox.py +16 -12
  10. pygpt_net/controller/config/field/checkbox_list.py +36 -31
  11. pygpt_net/controller/config/field/cmd.py +51 -57
  12. pygpt_net/controller/config/field/combo.py +33 -16
  13. pygpt_net/controller/config/field/dictionary.py +48 -55
  14. pygpt_net/controller/config/field/input.py +50 -32
  15. pygpt_net/controller/config/field/slider.py +40 -45
  16. pygpt_net/controller/config/field/textarea.py +20 -6
  17. pygpt_net/controller/config/placeholder.py +110 -231
  18. pygpt_net/controller/ctx/common.py +48 -49
  19. pygpt_net/controller/ctx/ctx.py +24 -4
  20. pygpt_net/controller/lang/mapping.py +57 -95
  21. pygpt_net/controller/lang/plugins.py +64 -55
  22. pygpt_net/controller/lang/settings.py +39 -38
  23. pygpt_net/controller/layout/layout.py +11 -2
  24. pygpt_net/controller/plugins/plugins.py +19 -1
  25. pygpt_net/controller/settings/profile.py +16 -4
  26. pygpt_net/controller/ui/mode.py +107 -125
  27. pygpt_net/core/bridge/bridge.py +5 -5
  28. pygpt_net/core/command/command.py +149 -219
  29. pygpt_net/core/ctx/ctx.py +94 -146
  30. pygpt_net/core/debug/debug.py +48 -58
  31. pygpt_net/core/models/models.py +74 -112
  32. pygpt_net/core/modes/modes.py +13 -21
  33. pygpt_net/core/plugins/plugins.py +154 -177
  34. pygpt_net/core/presets/presets.py +103 -176
  35. pygpt_net/core/render/web/body.py +2 -3
  36. pygpt_net/core/render/web/renderer.py +109 -180
  37. pygpt_net/core/text/utils.py +28 -44
  38. pygpt_net/core/tokens/tokens.py +104 -203
  39. pygpt_net/data/config/config.json +3 -3
  40. pygpt_net/data/config/models.json +3 -3
  41. pygpt_net/item/ctx.py +141 -139
  42. pygpt_net/plugin/agent/plugin.py +2 -1
  43. pygpt_net/plugin/audio_output/plugin.py +5 -2
  44. pygpt_net/plugin/base/plugin.py +77 -93
  45. pygpt_net/plugin/bitbucket/plugin.py +3 -2
  46. pygpt_net/plugin/cmd_code_interpreter/plugin.py +3 -2
  47. pygpt_net/plugin/cmd_custom/plugin.py +3 -2
  48. pygpt_net/plugin/cmd_files/plugin.py +3 -2
  49. pygpt_net/plugin/cmd_history/plugin.py +3 -2
  50. pygpt_net/plugin/cmd_mouse_control/plugin.py +5 -2
  51. pygpt_net/plugin/cmd_serial/plugin.py +3 -2
  52. pygpt_net/plugin/cmd_system/plugin.py +3 -6
  53. pygpt_net/plugin/cmd_web/plugin.py +3 -2
  54. pygpt_net/plugin/experts/plugin.py +2 -2
  55. pygpt_net/plugin/facebook/plugin.py +3 -4
  56. pygpt_net/plugin/github/plugin.py +4 -2
  57. pygpt_net/plugin/google/plugin.py +3 -3
  58. pygpt_net/plugin/idx_llama_index/plugin.py +3 -2
  59. pygpt_net/plugin/mailer/plugin.py +3 -5
  60. pygpt_net/plugin/openai_vision/plugin.py +3 -2
  61. pygpt_net/plugin/real_time/plugin.py +52 -60
  62. pygpt_net/plugin/slack/plugin.py +3 -4
  63. pygpt_net/plugin/telegram/plugin.py +3 -4
  64. pygpt_net/plugin/twitter/plugin.py +3 -4
  65. pygpt_net/tools/code_interpreter/tool.py +0 -1
  66. pygpt_net/tools/translator/tool.py +1 -1
  67. pygpt_net/ui/layout/ctx/ctx_list.py +10 -6
  68. pygpt_net/ui/main.py +33 -29
  69. pygpt_net/ui/tray.py +61 -60
  70. pygpt_net/ui/widget/lists/context.py +2 -2
  71. pygpt_net/ui/widget/textarea/web.py +18 -14
  72. {pygpt_net-2.6.3.dist-info → pygpt_net-2.6.5.dist-info}/METADATA +12 -2
  73. {pygpt_net-2.6.3.dist-info → pygpt_net-2.6.5.dist-info}/RECORD +76 -76
  74. {pygpt_net-2.6.3.dist-info → pygpt_net-2.6.5.dist-info}/LICENSE +0 -0
  75. {pygpt_net-2.6.3.dist-info → pygpt_net-2.6.5.dist-info}/WHEEL +0 -0
  76. {pygpt_net-2.6.3.dist-info → pygpt_net-2.6.5.dist-info}/entry_points.txt +0 -0
@@ -6,11 +6,12 @@
6
6
  # GitHub: https://github.com/szczyglis-dev/py-gpt #
7
7
  # MIT License #
8
8
  # Created By : Marcin Szczygliński #
9
- # Updated Date: 2025.08.11 14:00:00 #
9
+ # Updated Date: 2025.08.15 23:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import base64
13
- from typing import Optional
13
+ import io
14
+ from typing import Optional, Literal
14
15
 
15
16
  from PySide6.QtCore import QObject, Signal, Slot, QRunnable
16
17
 
@@ -20,6 +21,32 @@ from pygpt_net.core.types import MODE_ASSISTANT
20
21
  from pygpt_net.core.text.utils import has_unclosed_code_tag
21
22
  from pygpt_net.item.ctx import CtxItem
22
23
 
24
+ EventType = Literal[
25
+ "response.completed",
26
+ "response.output_text.delta",
27
+ "response.output_item.added",
28
+ "response.function_call_arguments.delta",
29
+ "response.function_call_arguments.done",
30
+ "response.output_text.annotation.added",
31
+ "response.reasoning_summary_text.delta",
32
+ "response.output_item.done",
33
+ "response.code_interpreter_call_code.delta",
34
+ "response.code_interpreter_call_code.done",
35
+ "response.image_generation_call.partial_image",
36
+ "response.created",
37
+ "response.done",
38
+ "response.failed",
39
+ "error",
40
+ ]
41
+ ChunkType = Literal[
42
+ "api_chat",
43
+ "api_chat_responses",
44
+ "api_completion",
45
+ "langchain_chat",
46
+ "llama_chat",
47
+ "raw",
48
+ ]
49
+
23
50
  class WorkerSignals(QObject):
24
51
  """
25
52
  Defines the signals available from a running worker thread.
@@ -54,15 +81,15 @@ class StreamWorker(QRunnable):
54
81
  output_tokens = 0
55
82
  begin = True
56
83
  error = None
57
- fn_args_buffers = {}
58
- citations = []
84
+ fn_args_buffers: dict[str, io.StringIO] = {}
85
+ citations: Optional[list] = []
59
86
  files = []
60
87
  img_path = core.image.gen_unique_path(ctx)
61
88
  is_image = False
62
89
  is_code = False
63
90
  force_func_call = False
64
91
  stopped = False
65
- chunk_type = "raw"
92
+ chunk_type: ChunkType = "raw"
66
93
  generator = ctx.stream
67
94
  ctx.stream = None
68
95
 
@@ -101,12 +128,12 @@ class StreamWorker(QRunnable):
101
128
  stopped = True
102
129
  break
103
130
 
104
- etype = None
131
+ etype: Optional[EventType] = None
105
132
  response = None
106
133
 
107
134
  if ctx.use_responses_api:
108
135
  if hasattr(chunk, 'type'):
109
- etype = chunk.type
136
+ etype = chunk.type # type: ignore[assignment]
110
137
  chunk_type = "api_chat_responses"
111
138
  else:
112
139
  continue
@@ -128,7 +155,6 @@ class StreamWorker(QRunnable):
128
155
  else:
129
156
  chunk_type = "raw"
130
157
 
131
- # OpenAI chat completion
132
158
  if chunk_type == "api_chat":
133
159
  citations = None
134
160
  delta = chunk.choices[0].delta
@@ -158,7 +184,6 @@ class StreamWorker(QRunnable):
158
184
  if getattr(tool_chunk.function, "arguments", None):
159
185
  tool_call["function"]["arguments"] += tool_chunk.function.arguments
160
186
 
161
- # OpenAI Responses API
162
187
  elif chunk_type == "api_chat_responses":
163
188
  if etype == "response.completed":
164
189
  for item in chunk.response.output:
@@ -197,7 +222,6 @@ class StreamWorker(QRunnable):
197
222
  elif etype == "response.output_text.delta":
198
223
  response = chunk.delta
199
224
 
200
- # function_call
201
225
  elif etype == "response.output_item.added" and chunk.item.type == "function_call":
202
226
  tool_calls.append({
203
227
  "id": chunk.item.id,
@@ -205,18 +229,23 @@ class StreamWorker(QRunnable):
205
229
  "type": "function",
206
230
  "function": {"name": chunk.item.name, "arguments": ""}
207
231
  })
208
- fn_args_buffers[chunk.item.id] = ""
232
+ fn_args_buffers[chunk.item.id] = io.StringIO()
209
233
  elif etype == "response.function_call_arguments.delta":
210
- fn_args_buffers[chunk.item_id] += chunk.delta
234
+ buf = fn_args_buffers.get(chunk.item_id)
235
+ if buf is not None:
236
+ buf.write(chunk.delta)
211
237
  elif etype == "response.function_call_arguments.done":
212
238
  buf = fn_args_buffers.pop(chunk.item_id, None)
213
239
  if buf is not None:
240
+ try:
241
+ args_val = buf.getvalue()
242
+ finally:
243
+ buf.close()
214
244
  for tc in tool_calls:
215
245
  if tc["id"] == chunk.item_id:
216
- tc["function"]["arguments"] = buf
246
+ tc["function"]["arguments"] = args_val
217
247
  break
218
248
 
219
- # annotations
220
249
  elif etype == "response.output_text.annotation.added":
221
250
  ann = chunk.annotation
222
251
  if ann['type'] == "url_citation":
@@ -231,7 +260,6 @@ class StreamWorker(QRunnable):
231
260
  "file_id": ann['file_id'],
232
261
  })
233
262
 
234
- # computer use
235
263
  elif etype == "response.reasoning_summary_text.delta":
236
264
  response = chunk.delta
237
265
 
@@ -240,7 +268,6 @@ class StreamWorker(QRunnable):
240
268
  if has_calls:
241
269
  force_func_call = True
242
270
 
243
- # code interpreter
244
271
  elif etype == "response.code_interpreter_call_code.delta":
245
272
  if not is_code:
246
273
  response = "\n\n**Code interpreter**\n```python\n" + chunk.delta
@@ -250,15 +277,14 @@ class StreamWorker(QRunnable):
250
277
  elif etype == "response.code_interpreter_call_code.done":
251
278
  response = "\n\n```\n-----------\n"
252
279
 
253
- # image gen
254
280
  elif etype == "response.image_generation_call.partial_image":
255
281
  image_base64 = chunk.partial_image_b64
256
282
  image_bytes = base64.b64decode(image_base64)
257
283
  with open(img_path, "wb") as f:
258
284
  f.write(image_bytes)
285
+ del image_bytes
259
286
  is_image = True
260
287
 
261
- # response ID
262
288
  elif etype == "response.created":
263
289
  ctx.msg_id = str(chunk.response.id)
264
290
  core.ctx.update_item(ctx)
@@ -266,18 +292,15 @@ class StreamWorker(QRunnable):
266
292
  elif etype in {"response.done", "response.failed", "error"}:
267
293
  pass
268
294
 
269
- # OpenAI completion
270
295
  elif chunk_type == "api_completion":
271
296
  choice0 = chunk.choices[0]
272
297
  if choice0.text is not None:
273
298
  response = choice0.text
274
299
 
275
- # langchain chat
276
300
  elif chunk_type == "langchain_chat":
277
301
  if chunk.content is not None:
278
302
  response = str(chunk.content)
279
303
 
280
- # llama chat
281
304
  elif chunk_type == "llama_chat":
282
305
  if chunk.delta is not None:
283
306
  response = str(chunk.delta)
@@ -301,7 +324,6 @@ class StreamWorker(QRunnable):
301
324
  tool_calls.clear()
302
325
  tool_calls.append(tool_call)
303
326
 
304
- # raw text (llama-index / langchain completion)
305
327
  else:
306
328
  if chunk is not None:
307
329
  response = str(chunk)
@@ -326,13 +348,11 @@ class StreamWorker(QRunnable):
326
348
 
327
349
  chunk = None
328
350
 
329
- # tool calls
330
351
  if tool_calls:
331
352
  ctx.force_call = force_func_call
332
353
  core.debug.info("[chat] Tool calls found, unpacking...")
333
354
  core.command.unpack_tool_calls_chunks(ctx, tool_calls)
334
355
 
335
- # image
336
356
  if is_image:
337
357
  core.debug.info("[chat] Image generation call found")
338
358
  ctx.images = [img_path]
@@ -371,11 +391,17 @@ class StreamWorker(QRunnable):
371
391
 
372
392
  emit_end(ctx)
373
393
 
394
+ for _buf in fn_args_buffers.values():
395
+ try:
396
+ _buf.close()
397
+ except Exception:
398
+ pass
374
399
  fn_args_buffers.clear()
375
400
  files.clear()
376
401
  tool_calls.clear()
377
- if citations is not None:
402
+ if citations is not None and citations is not ctx.urls:
378
403
  citations.clear()
404
+ citations = None
379
405
 
380
406
  self.cleanup()
381
407
 
@@ -6,7 +6,7 @@
6
6
  # GitHub: https://github.com/szczyglis-dev/py-gpt #
7
7
  # MIT License #
8
8
  # Created By : Marcin Szczygliński #
9
- # Updated Date: 2025.06.30 02:00:00 #
9
+ # Updated Date: 2025.08.15 23:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  from typing import Any, Dict, List
@@ -40,6 +40,24 @@ class Config:
40
40
  self.slider = Slider(window)
41
41
  self.textarea = Textarea(window)
42
42
 
43
+ self._apply_map = {
44
+ 'text': self.input.apply,
45
+ 'textarea': self.textarea.apply,
46
+ 'bool': self.checkbox.apply,
47
+ 'bool_list': self.checkbox_list.apply,
48
+ 'dict': self.dictionary.apply,
49
+ 'combo': self.combo.apply,
50
+ 'cmd': self.cmd.apply,
51
+ }
52
+ self._get_map = {
53
+ 'text': self.input.get_value,
54
+ 'textarea': self.textarea.get_value,
55
+ 'bool': self.checkbox.get_value,
56
+ 'bool_list': self.checkbox_list.get_value,
57
+ 'dict': self.dictionary.get_value,
58
+ 'cmd': self.cmd.get_value,
59
+ }
60
+
43
61
  def load_options(
44
62
  self,
45
63
  parent_id: str,
@@ -51,8 +69,7 @@ class Config:
51
69
  :param parent_id: Parent ID
52
70
  :param options: Options dict
53
71
  """
54
- for key in options:
55
- option = options[key]
72
+ for key, option in options.items():
56
73
  self.apply(parent_id, key, option)
57
74
 
58
75
  def apply(
@@ -68,25 +85,16 @@ class Config:
68
85
  :param key: Option key
69
86
  :param option: Option dict
70
87
  """
71
- if option['type'] == 'int' or option['type'] == 'float':
72
- if 'slider' in option and option['slider']:
88
+ t = option['type']
89
+ if t in ('int', 'float'):
90
+ if option.get('slider'):
73
91
  self.slider.apply(parent_id, key, option)
74
92
  else:
75
93
  self.input.apply(parent_id, key, option)
76
- elif option['type'] == 'text':
77
- self.input.apply(parent_id, key, option)
78
- elif option['type'] == 'textarea':
79
- self.textarea.apply(parent_id, key, option)
80
- elif option['type'] == 'bool':
81
- self.checkbox.apply(parent_id, key, option)
82
- elif option['type'] == 'bool_list':
83
- self.checkbox_list.apply(parent_id, key, option)
84
- elif option['type'] == 'dict':
85
- self.dictionary.apply(parent_id, key, option)
86
- elif option['type'] == 'combo':
87
- self.combo.apply(parent_id, key, option)
88
- elif option['type'] == 'cmd':
89
- self.cmd.apply(parent_id, key, option)
94
+ return
95
+ func = self._apply_map.get(t)
96
+ if func:
97
+ func(parent_id, key, option)
90
98
 
91
99
  def apply_value(
92
100
  self,
@@ -122,25 +130,16 @@ class Config:
122
130
  :param idx: return selected idx, not the value
123
131
  :return: Option value
124
132
  """
125
- if option['type'] == 'int' or option['type'] == 'float':
126
- if 'slider' in option and option['slider']:
133
+ t = option['type']
134
+ if t in ('int', 'float'):
135
+ if option.get('slider'):
127
136
  return self.slider.get_value(parent_id, key, option)
128
- else:
129
- return self.input.get_value(parent_id, key, option)
130
- elif option['type'] == 'text':
131
137
  return self.input.get_value(parent_id, key, option)
132
- elif option['type'] == 'textarea':
133
- return self.textarea.get_value(parent_id, key, option)
134
- elif option['type'] == 'bool':
135
- return self.checkbox.get_value(parent_id, key, option)
136
- elif option['type'] == 'bool_list':
137
- return self.checkbox_list.get_value(parent_id, key, option)
138
- elif option['type'] == 'dict':
139
- return self.dictionary.get_value(parent_id, key, option)
140
- elif option['type'] == 'combo':
138
+ if t == 'combo':
141
139
  return self.combo.get_value(parent_id, key, option, idx)
142
- elif option['type'] == 'cmd':
143
- return self.cmd.get_value(parent_id, key, option)
140
+ func = self._get_map.get(t)
141
+ if func:
142
+ return func(parent_id, key, option)
144
143
 
145
144
  def update_list(
146
145
  self,
@@ -159,13 +158,11 @@ class Config:
159
158
  """
160
159
  if "type" not in option:
161
160
  return
162
- if option['type'] == 'combo':
163
- as_dict = {}
164
- for item in items:
165
- for k, v in item.items():
166
- as_dict[k] = v
161
+ t = option['type']
162
+ if t == 'combo':
163
+ as_dict = {k: v for d in items for k, v in d.items()}
167
164
  self.update_combo(parent_id, key, as_dict)
168
- elif option['type'] == 'bool_list':
165
+ elif t == 'bool_list':
169
166
  self.update_bool_list(parent_id, key, items)
170
167
 
171
168
  def update_combo(
@@ -196,4 +193,4 @@ class Config:
196
193
  :param key: Option key
197
194
  :param items: Items dict
198
195
  """
199
- self.checkbox_list.update_list(parent_id, key, items)
196
+ self.checkbox_list.update_list(parent_id, key, items)
@@ -6,7 +6,7 @@
6
6
  # GitHub: https://github.com/szczyglis-dev/py-gpt #
7
7
  # MIT License #
8
8
  # Created By : Marcin Szczygliński #
9
- # Updated Date: 2025.06.29 18:00:00 #
9
+ # Updated Date: 2025.08.15 23:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  from typing import Any, Dict
@@ -26,7 +26,7 @@ class Checkbox:
26
26
  parent_id: str,
27
27
  key: str,
28
28
  option: Dict[str, Any]
29
- ):
29
+ ) -> None:
30
30
  """
31
31
  Apply value to checkbox
32
32
 
@@ -39,17 +39,19 @@ class Checkbox:
39
39
  value = option["value"]
40
40
  if value is None:
41
41
  value = False
42
- if key in self.window.ui.config[parent_id]:
43
- self.window.ui.config[parent_id][key].box.setChecked(bool(value))
42
+ cfg_parent = self.window.ui.config[parent_id]
43
+ row = cfg_parent.get(key)
44
+ if row is not None:
45
+ row.box.setChecked(bool(value))
44
46
 
45
47
  def on_update(
46
48
  self,
47
49
  parent_id: str,
48
50
  key: str,
49
- option: dict,
51
+ option: Dict[str, Any],
50
52
  value: Any,
51
53
  hooks: bool = True
52
- ):
54
+ ) -> None:
53
55
  """
54
56
  Event: on update checkbox value
55
57
 
@@ -59,11 +61,11 @@ class Checkbox:
59
61
  :param value: Option value
60
62
  :param hooks: Run hooks
61
63
  """
62
- # on update hooks
63
64
  if hooks:
64
- hook_name = "update.{}.{}".format(parent_id, key)
65
- if self.window.ui.has_hook(hook_name):
66
- hook = self.window.ui.get_hook(hook_name)
65
+ ui = self.window.ui
66
+ hook_name = f"update.{parent_id}.{key}"
67
+ if ui.has_hook(hook_name):
68
+ hook = ui.get_hook(hook_name)
67
69
  try:
68
70
  hook(key, value, 'checkbox')
69
71
  except Exception as e:
@@ -83,6 +85,8 @@ class Checkbox:
83
85
  :param option: Option data dict
84
86
  :return: Option value
85
87
  """
86
- if key not in self.window.ui.config[parent_id]:
88
+ cfg_parent = self.window.ui.config[parent_id]
89
+ row = cfg_parent.get(key)
90
+ if row is None:
87
91
  return False
88
- return self.window.ui.config[parent_id][key].box.isChecked()
92
+ return row.box.isChecked()
@@ -6,7 +6,7 @@
6
6
  # GitHub: https://github.com/szczyglis-dev/py-gpt #
7
7
  # MIT License #
8
8
  # Created By : Marcin Szczygliński #
9
- # Updated Date: 2025.06.30 02:00:00 #
9
+ # Updated Date: 2025.08.15 23:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  from typing import Any, Dict, List
@@ -37,19 +37,24 @@ class CheckboxList:
37
37
  if "value" not in option:
38
38
  return
39
39
  value = option["value"]
40
- exploded_list = value.split(",") if isinstance(value, str) else []
41
- if key not in self.window.ui.config[parent_id]:
42
- self.window.ui.config[parent_id][key] = {}
43
- for item in self.window.ui.config[parent_id][key].boxes:
44
- if self.window.ui.config[parent_id][key].boxes[item] is not None:
45
- self.window.ui.config[parent_id][key].boxes[item].setChecked(False)
46
- for item in exploded_list:
47
- item = item.strip()
48
- if item not in self.window.ui.config[parent_id][key].boxes:
49
- continue
50
- if self.window.ui.config[parent_id][key].boxes[item] is None:
40
+ selection = {s.strip() for s in value.split(",")} if isinstance(value, str) else set()
41
+ selection.discard("")
42
+
43
+ ui = self.window.ui
44
+ cfg_parent = ui.config.get(parent_id)
45
+ if not cfg_parent:
46
+ return
47
+ entry = cfg_parent.get(key)
48
+ if entry is None or not hasattr(entry, "boxes"):
49
+ return
50
+ boxes = entry.boxes
51
+
52
+ for name, cb in boxes.items():
53
+ if cb is None:
51
54
  continue
52
- self.window.ui.config[parent_id][key].boxes[item].setChecked(True)
55
+ desired = name in selection
56
+ if cb.isChecked() != desired:
57
+ cb.setChecked(desired)
53
58
 
54
59
  def on_update(
55
60
  self,
@@ -70,15 +75,16 @@ class CheckboxList:
70
75
  :param subkey: Subkey for specific checkbox
71
76
  :param hooks: Run hooks
72
77
  """
73
- # on update hooks
74
78
  if hooks:
75
- hook_name = "update.{}.{}".format(parent_id, key)
76
- if self.window.ui.has_hook(hook_name):
77
- hook = self.window.ui.get_hook(hook_name)
78
- try:
79
- hook(key, value, 'bool_list')
80
- except Exception as e:
81
- self.window.core.debug.log(e)
79
+ ui = self.window.ui
80
+ hook_name = f"update.{parent_id}.{key}"
81
+ if ui.has_hook(hook_name):
82
+ hook = ui.get_hook(hook_name)
83
+ if hook:
84
+ try:
85
+ hook(key, value, 'bool_list')
86
+ except Exception as e:
87
+ self.window.core.debug.log(e)
82
88
 
83
89
  def get_value(
84
90
  self,
@@ -94,16 +100,15 @@ class CheckboxList:
94
100
  :param option: Option data dict
95
101
  :return: Option value
96
102
  """
97
- if key not in self.window.ui.config[parent_id]:
103
+ ui = self.window.ui
104
+ cfg_parent = ui.config.get(parent_id)
105
+ if not cfg_parent:
98
106
  return ""
99
- imploded_list = []
100
- for item in self.window.ui.config[parent_id][key].boxes:
101
- if self.window.ui.config[parent_id][key].boxes[item] is None:
102
- continue
103
- if self.window.ui.config[parent_id][key].boxes[item].isChecked():
104
- imploded_list.append(item)
105
- return ",".join(imploded_list)
106
-
107
+ entry = cfg_parent.get(key)
108
+ if entry is None or not hasattr(entry, "boxes"):
109
+ return ""
110
+ boxes = entry.boxes
111
+ return ",".join(name for name, cb in boxes.items() if cb is not None and cb.isChecked())
107
112
 
108
113
  def update_list(
109
114
  self,
@@ -118,4 +123,4 @@ class CheckboxList:
118
123
  :param key: Option key
119
124
  :param items: Items dict
120
125
  """
121
- self.window.ui.config[parent_id][key].update_boxes_list(items)
126
+ self.window.ui.config[parent_id][key].update_boxes_list(items)