pgwidgets-python 0.3.3__tar.gz → 0.3.5__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.
Files changed (77) hide show
  1. {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/PKG-INFO +2 -2
  2. {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/pgwidgets/async_/application.py +8 -0
  3. {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/pgwidgets/extras/file_browser.py +4 -0
  4. {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/pgwidgets/method_types.py +30 -4
  5. {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/pgwidgets/sync/application.py +20 -0
  6. pgwidgets_python-0.3.5/pgwidgets/sync/text_source.py +200 -0
  7. {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/pgwidgets/sync/widget.py +5 -0
  8. pgwidgets_python-0.3.5/pgwidgets/text_model.py +792 -0
  9. {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/pgwidgets_python.egg-info/PKG-INFO +2 -2
  10. {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/pgwidgets_python.egg-info/SOURCES.txt +3 -0
  11. {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/pgwidgets_python.egg-info/requires.txt +1 -1
  12. {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/pgwidgets_python.egg-info/scm_file_list.json +3 -0
  13. pgwidgets_python-0.3.5/pgwidgets_python.egg-info/scm_version.json +8 -0
  14. {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/pyproject.toml +7 -5
  15. pgwidgets_python-0.3.5/tests/test_text_source.py +161 -0
  16. pgwidgets_python-0.3.3/pgwidgets_python.egg-info/scm_version.json +0 -8
  17. {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/.flake8 +0 -0
  18. {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/.github/workflows/tests.yml +0 -0
  19. {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/.gitignore +0 -0
  20. {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/.readthedocs.yaml +0 -0
  21. {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/LICENSE.md +0 -0
  22. {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/README.md +0 -0
  23. {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/docs/Makefile +0 -0
  24. {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/docs/WhatsNew.rst +0 -0
  25. {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/docs/api/async.rst +0 -0
  26. {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/docs/api/index.rst +0 -0
  27. {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/docs/api/sync.rst +0 -0
  28. {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/docs/architecture.rst +0 -0
  29. {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/docs/async.rst +0 -0
  30. {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/docs/callbacks.rst +0 -0
  31. {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/docs/conf.py +0 -0
  32. {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/docs/extras.rst +0 -0
  33. {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/docs/getting-started.rst +0 -0
  34. {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/docs/index.rst +0 -0
  35. {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/docs/subclassing.rst +0 -0
  36. {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/docs/sync.rst +0 -0
  37. {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/docs/utilities.rst +0 -0
  38. {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/docs/web-servers.rst +0 -0
  39. {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/docs/widgets.rst +0 -0
  40. {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/examples/README.md +0 -0
  41. {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/examples/all_widgets.py +0 -0
  42. {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/examples/all_widgets_async.py +0 -0
  43. {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/examples/demo_async.py +0 -0
  44. {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/examples/demo_sync.py +0 -0
  45. {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/examples/demo_treeview.py +0 -0
  46. {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/examples/flask-multi-process/README.md +0 -0
  47. {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/examples/flask-multi-process/gunicorn.conf.py +0 -0
  48. {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/examples/flask-multi-process/nginx.conf +0 -0
  49. {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/examples/flask-multi-process/server.py +0 -0
  50. {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/examples/flask-multi-process/user_app.py +0 -0
  51. {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/pgwidgets/__init__.py +0 -0
  52. {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/pgwidgets/_json.py +0 -0
  53. {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/pgwidgets/async_/Widgets.py +0 -0
  54. {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/pgwidgets/async_/__init__.py +0 -0
  55. {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/pgwidgets/async_/widget.py +0 -0
  56. {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/pgwidgets/buffer.py +0 -0
  57. {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/pgwidgets/callbacks.py +0 -0
  58. {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/pgwidgets/defs.py +0 -0
  59. {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/pgwidgets/extras/__init__.py +0 -0
  60. {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/pgwidgets/icons/pgw-python-logo.png +0 -0
  61. {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/pgwidgets/sync/Widgets.py +0 -0
  62. {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/pgwidgets/sync/__init__.py +0 -0
  63. {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/pgwidgets_python.egg-info/dependency_links.txt +0 -0
  64. {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/pgwidgets_python.egg-info/top_level.txt +0 -0
  65. {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/setup.cfg +0 -0
  66. {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/test_pg2.py +0 -0
  67. {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/tests/__init__.py +0 -0
  68. {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/tests/test_async_session.py +0 -0
  69. {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/tests/test_defs.py +0 -0
  70. {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/tests/test_extras.py +0 -0
  71. {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/tests/test_fonts.py +0 -0
  72. {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/tests/test_protocol.py +0 -0
  73. {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/tests/test_reconstruct.py +0 -0
  74. {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/tests/test_resolve_kwargs.py +0 -0
  75. {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/tests/test_session.py +0 -0
  76. {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/tests/test_stateful.py +0 -0
  77. {pgwidgets_python-0.3.3 → pgwidgets_python-0.3.5}/tests/test_widget_classes.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pgwidgets-python
3
- Version: 0.3.3
3
+ Version: 0.3.5
4
4
  Summary: Python bindings for the pgwidgets JavaScript widget library
5
5
  Author: PGWidgets Developers
6
6
  License: BSD-3-Clause
@@ -17,7 +17,7 @@ Classifier: Topic :: Software Development :: User Interfaces
17
17
  Requires-Python: >=3.12
18
18
  Description-Content-Type: text/markdown
19
19
  License-File: LICENSE.md
20
- Requires-Dist: pgwidgets-js>=0.2.0
20
+ Requires-Dist: pgwidgets-js>=0.3.4
21
21
  Requires-Dist: websockets>=12
22
22
  Provides-Extra: dev
23
23
  Requires-Dist: sphinx; extra == "dev"
@@ -434,6 +434,14 @@ class Session:
434
434
  widget._state[spec[0]] = tuple(args)
435
435
  else:
436
436
  widget._state[spec] = args[0]
437
+ # ComboBox: a value committed with Enter in the browser may be
438
+ # a new entry the browser appended to its list; mirror that
439
+ # append so our _items (and thus get_index) stays consistent.
440
+ if (widget._js_class == "ComboBox" and action == "activated"
441
+ and len(args) >= 2):
442
+ items = widget._state.setdefault("_items", [])
443
+ if args[1] not in items:
444
+ items.append(args[1])
437
445
 
438
446
  # Dialog autoclose: the JS side auto-hides when a button is
439
447
  # clicked. Sync that to Python so reconstruction doesn't
@@ -209,6 +209,10 @@ class FileBrowser(Callbacks):
209
209
 
210
210
  # ── Public API ──────────────────────────────────────────────
211
211
 
212
+ def set_title(self, title):
213
+ """Set the dialog's title bar text."""
214
+ self._dialog.set_title(title)
215
+
212
216
  def set_mode(self, mode):
213
217
  """Set the selection mode: 'file', 'files', 'directory', 'save'."""
214
218
  self._mode = mode
@@ -62,7 +62,8 @@ JS_ONLY_METHODS = {
62
62
  # MDIWidget
63
63
  "get_subwin", "get_subwindows", "get_configuration",
64
64
  "get_child_size", "get_child_position",
65
- # Button icon (set_icon is an action, so get_icon must round-trip)
65
+ # Button icon: set_icon stores state (so it reconstructs); get_icon
66
+ # reads the live browser value, so it round-trips.
66
67
  "get_icon",
67
68
  }
68
69
 
@@ -107,7 +108,7 @@ ACTION_METHODS = {
107
108
  # Timer
108
109
  "start", "cancel", "set", "cond_set",
109
110
  # Table/Tree row-level modifications (tracked via bulk set_data)
110
- "add_item", "remove_item", "update_tree", "remove_items",
111
+ "add_item", "remove_item", "update_tree", "remove_items", "delete_tree",
111
112
  "insert_row", "append_row", "delete_row",
112
113
  "insert_column", "append_column", "delete_column",
113
114
  "set_cell",
@@ -121,8 +122,6 @@ ACTION_METHODS = {
121
122
  "set_minimum_size",
122
123
  # TreeView per-column
123
124
  "set_column_width", "set_column_editable",
124
- # TextSource per-line
125
- "set_icon",
126
125
  }
127
126
 
128
127
  # Setter methods that DON'T follow the set_* naming convention.
@@ -420,7 +419,34 @@ BINARY_STATE_KEYS = {
420
419
  }
421
420
 
422
421
 
422
+ def _combobox_get_index(self):
423
+ """Index of the item matching the current text, or -1 when the text is
424
+ not one of the offerings (e.g. a value typed into an editable combo box
425
+ but not yet committed with Enter). Mirrors the JS get_index(), which
426
+ is items.indexOf(input value), and stays consistent with get_text()
427
+ (the cached entry text, kept live by the 'modified' callback)."""
428
+ items = self._state.get("_items", [])
429
+ try:
430
+ return items.index(self._state.get("text", ""))
431
+ except ValueError:
432
+ return -1
433
+
434
+
435
+ def _combobox_set_index(self, idx):
436
+ """Select the item at *idx*, keeping the cached text consistent with
437
+ the index so get_text() agrees with get_index() after a programmatic
438
+ change (the generic setter would leave 'text' stale)."""
439
+ items = self._state.get("_items", [])
440
+ if 0 <= idx < len(items):
441
+ self._state["text"] = items[idx]
442
+ self._state["index"] = idx
443
+ self._user_set_state.add("index")
444
+ return self._call("set_index", idx)
445
+
446
+
423
447
  CUSTOM_METHODS = {
448
+ ("ComboBox", "get_index"): _combobox_get_index,
449
+ ("ComboBox", "set_index"): _combobox_set_index,
424
450
  ("TabWidget", "index_to_widget"): _index_to_widget,
425
451
  ("TabWidget", "index_of"): _index_of,
426
452
  ("StackWidget", "index_to_widget"): _index_to_widget,
@@ -524,6 +524,14 @@ class Session:
524
524
  widget._state[spec[0]] = tuple(args)
525
525
  else:
526
526
  widget._state[spec] = args[0]
527
+ # ComboBox: a value committed with Enter in the browser may be
528
+ # a new entry the browser appended to its list; mirror that
529
+ # append so our _items (and thus get_index) stays consistent.
530
+ if (widget._js_class == "ComboBox" and action == "activated"
531
+ and len(args) >= 2):
532
+ items = widget._state.setdefault("_items", [])
533
+ if args[1] not in items:
534
+ items.append(args[1])
527
535
  # Dialog autoclose: the JS side auto-hides when a button is
528
536
  # clicked. Sync that to Python so reconstruction doesn't
529
537
  # re-show a dialog that was autoclosed.
@@ -1399,6 +1407,18 @@ class Session:
1399
1407
  # callbacks transferred by _transfer_proxy during step 3.
1400
1408
  self._reregister_callbacks(widget)
1401
1409
 
1410
+ # 6. Widgets with a Python-authoritative model (TextSource) push
1411
+ # their full model state (text, tags, cursor) to the freshly
1412
+ # (re)connected browser. Guard it: a failure here must not abort
1413
+ # reconstruction of the whole widget tree.
1414
+ if hasattr(widget, "_reconstruct_model"):
1415
+ try:
1416
+ widget._reconstruct_model()
1417
+ except Exception:
1418
+ self._logger.error(
1419
+ "TextSource model reconstruction failed for wid=%s",
1420
+ getattr(widget, "_wid", "?"), exc_info=True)
1421
+
1402
1422
  def _reregister_callbacks(self, widget):
1403
1423
  """Re-register user callbacks and auto-sync listeners on *widget*.
1404
1424
 
@@ -0,0 +1,200 @@
1
+ """Python-authoritative TextSource for the synchronous API.
2
+
3
+ Unlike most widgets (whose state is a handful of scalars), a text buffer
4
+ has rich structure -- content, live position refs, a tag table, cursor and
5
+ selection. We keep the *authoritative* copy of all of that on the Python
6
+ side in a :class:`~pgwidgets.text_model.TextModel`, so the whole API works
7
+ before a browser is ever connected (the UI can be built up headless and
8
+ replayed on connect). The browser is driven as a *view*: model changes are
9
+ pushed as offset-based operations through the render hooks, and are no-ops
10
+ (``_call`` returns None) until a browser attaches, at which point
11
+ ``_reconstruct_model`` replays the full state.
12
+
13
+ Refs never cross the wire -- they are ordinary Python ``TextBufferRef``
14
+ objects owned by the local model. (The in-situ / JS-only case, where the
15
+ browser is authoritative, is handled by the separate pgwidgets-js/pyodide
16
+ wrapper, not here.)
17
+
18
+ The class subclasses the *generated* TextSource proxy so it inherits all the
19
+ base-widget methods (show/hide/set_tooltip/get_size/...) and the widget
20
+ option setters (set_editable/set_wrap/scrolling); it overrides only the
21
+ methods that manipulate the text model.
22
+ """
23
+
24
+ from pgwidgets.sync.widget import build_widget_class
25
+ from pgwidgets.defs import WIDGETS
26
+ from pgwidgets.text_model import TextModel
27
+
28
+
29
+ _GeneratedTextSource = build_widget_class("TextSource", WIDGETS["TextSource"])
30
+
31
+
32
+ class _BrowserTextModel(TextModel):
33
+ """A TextModel whose render hooks push offset-based operations to the
34
+ browser via the owning widget. When no browser is connected the calls
35
+ are no-ops, so the model is fully usable headless."""
36
+
37
+ def __init__(self, widget):
38
+ self._widget = widget
39
+ super().__init__()
40
+
41
+ def _push(self, method, *args):
42
+ # _call round-trips when a browser is connected and is a no-op
43
+ # returning None otherwise.
44
+ self._widget._call(method, *args)
45
+
46
+ # -- render hooks (see TextModel) --
47
+ def _set_editor_text(self, text):
48
+ self._push("set_text", text)
49
+
50
+ def _replace_editor_range(self, start, end, new_text):
51
+ # Drive the JS buffer by offset; pushUndo=False because the Python
52
+ # model owns undo/redo.
53
+ self._push("_replaceRange", start, end, new_text, {"pushUndo": False})
54
+
55
+ def _apply_all_formats(self, region=None):
56
+ # Push the current tag intervals; the JS side re-renders styling.
57
+ # (Tag *definitions* are pushed by TextSource.create_tag.)
58
+ self._push("_restoreTagIntervals", self._tag_intervals())
59
+
60
+ def _apply_selection_to_editor(self):
61
+ if self._sel_start == self._sel_end:
62
+ self._push("_setCursorOffset", self._cursor)
63
+ else:
64
+ self._push("_setSelectionOffsets", self._sel_start, self._sel_end)
65
+
66
+ def _refresh_icon_gutter(self):
67
+ # TODO: render ref-anchored gutter icons in the browser.
68
+ pass
69
+
70
+ def _mark_clean(self):
71
+ pass
72
+
73
+ def _tag_intervals(self):
74
+ return [{"name": t["name"], "start": t["start"], "end": t["end"]}
75
+ for t in self._tags]
76
+
77
+
78
+ # Model methods delegated verbatim to self._model. Refs in/out are the
79
+ # model's own TextBufferRefs; nothing here touches the wire. These override
80
+ # the generated round-trip proxies of the same name.
81
+ _DELEGATED = (
82
+ # content
83
+ "get_length", "get_text", "get_text_range", "clear", "set_text",
84
+ "insert_text", "delete_range",
85
+ # refs
86
+ "create_ref", "remove_ref", "create_named_ref", "get_named_ref",
87
+ "remove_named_ref", "get_ref_start", "get_ref_end", "get_ref_bounds",
88
+ "get_ref_line_start", "get_ref_line_end", "set_icon",
89
+ # tags (create_tag handled explicitly to also push the tag definition)
90
+ "remove_tag_def", "has_tag", "apply_tag", "remove_tag",
91
+ "get_tags_at", "get_tags_range", "get_tag_region", "get_tag_regions",
92
+ # find / replace
93
+ "find", "find_all", "replace",
94
+ # cursor / selection
95
+ "get_cursor", "set_cursor", "has_selection", "get_selection_range",
96
+ "get_selection_bounds", "set_selection_range",
97
+ # undo / redo / dirty flag
98
+ "can_undo", "can_redo", "undo", "redo", "get_modified", "mark_clean",
99
+ # misc convenience
100
+ "get_end_lineno", "clear_icons",
101
+ )
102
+
103
+ # Callback actions that are model-authoritative (fire from the Python model)
104
+ # rather than being browser events.
105
+ _MODEL_CALLBACKS = {"changed", "cursor-moved", "cursor_moved"}
106
+
107
+
108
+ class TextSource(_GeneratedTextSource):
109
+
110
+ def __init__(self, session, *args, **kwargs):
111
+ super().__init__(session, *args, **kwargs)
112
+ # The authoritative model. Seed its text from the initial value the
113
+ # generated __init__ recorded (the JS side already has it from the
114
+ # constructor, so no push). Text/tags/cursor are reconstructed by
115
+ # _reconstruct_model(), not by the generic state replay, so drop
116
+ # 'text' from _state.
117
+ self._model = _BrowserTextModel(self)
118
+ text = self._state.get("text")
119
+ self._model._text = "" if text is None else str(text)
120
+ self._state.pop("text", None)
121
+
122
+ def create_tag(self, name, attrs=None, **kwdargs):
123
+ """Define a display tag locally and push its styling to the browser."""
124
+ merged = {} if attrs is None else dict(attrs)
125
+ merged.update(kwdargs)
126
+ self._model.create_tag(name, merged)
127
+ self._call("create_tag", name, merged)
128
+
129
+ def scroll_to_ref(self, ref):
130
+ """Scroll the view to a ref, addressed by its current offset."""
131
+ self._call("_scrollToOffset", self._model._offset_of(ref))
132
+
133
+ def scroll_to_lineno(self, lineno):
134
+ """Scroll so that line ``lineno`` is visible."""
135
+ self._call("_scrollToOffset",
136
+ self._model._offset_of_line_start(lineno))
137
+
138
+ def scroll_to_end(self):
139
+ """Scroll to the end of the buffer."""
140
+ self._call("_scrollToOffset", self._model.get_length())
141
+
142
+ def show_tooltip(self, text, x, y):
143
+ """Show a hover tooltip (``text``) near viewport point (x, y), or
144
+ hide it when ``text`` is empty. Normally called from a handler of
145
+ the 'tooltip' callback."""
146
+ self._call("_showTooltip", text, x, y)
147
+
148
+ # -- callbacks: 'changed'/'cursor_moved' are model-authoritative --
149
+ def add_callback(self, action, handler, *extra_args, **extra_kwargs):
150
+ if action in _MODEL_CALLBACKS:
151
+ self._model.add_callback(
152
+ self._model_cb_name(action),
153
+ lambda _m, *a: handler(self, *a, *extra_args, **extra_kwargs))
154
+ return
155
+ super().add_callback(action, handler, *extra_args, **extra_kwargs)
156
+
157
+ def on(self, action, handler, *extra_args, **extra_kwargs):
158
+ if action in _MODEL_CALLBACKS:
159
+ self._model.add_callback(
160
+ self._model_cb_name(action),
161
+ lambda _m, *a: handler(*a, *extra_args, **extra_kwargs))
162
+ return
163
+ super().on(action, handler, *extra_args, **extra_kwargs)
164
+
165
+ @staticmethod
166
+ def _model_cb_name(action):
167
+ return "cursor-moved" if action in ("cursor_moved",
168
+ "cursor-moved") else "changed"
169
+
170
+ # -- reconstruction: push the full model state to a freshly-connected
171
+ # browser (called by Session._reconstruct_widget after the generic
172
+ # create/state/callback replay) --
173
+ def _reconstruct_model(self):
174
+ m = self._model
175
+ self._call("set_text", m.get_text())
176
+ for name, attrs in m._tag_defs.items():
177
+ self._call("create_tag", name, attrs)
178
+ intervals = m._tag_intervals()
179
+ if intervals:
180
+ self._call("_restoreTagIntervals", intervals)
181
+ if m._sel_start != m._sel_end:
182
+ self._call("_setSelectionOffsets", m._sel_start, m._sel_end)
183
+ else:
184
+ self._call("_setCursorOffset", m._cursor)
185
+
186
+
187
+ def _install_delegators():
188
+ """Override the generated round-trip proxies with model-delegating
189
+ methods (defined at class scope so ``hasattr(TextSource, name)`` holds)."""
190
+ for _name in _DELEGATED:
191
+ def _make(n):
192
+ def method(self, *args, **kwargs):
193
+ return getattr(self._model, n)(*args, **kwargs)
194
+ method.__name__ = n
195
+ method.__qualname__ = "TextSource." + n
196
+ return method
197
+ setattr(TextSource, _name, _make(_name))
198
+
199
+
200
+ _install_delegators()
@@ -965,4 +965,9 @@ def build_all_widget_classes():
965
965
  classes = {}
966
966
  for js_class, defn in WIDGETS.items():
967
967
  classes[js_class] = build_widget_class(js_class, defn)
968
+ # TextSource is hand-written (Python-authoritative local text model)
969
+ # rather than generated -- see pgwidgets.sync.text_source. Deferred
970
+ # import avoids a circular import at module load.
971
+ from pgwidgets.sync.text_source import TextSource as _TextSource
972
+ classes['TextSource'] = _TextSource
968
973
  return classes