pgwidgets-python 0.3.2__tar.gz → 0.3.4__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 (76) hide show
  1. {pgwidgets_python-0.3.2 → pgwidgets_python-0.3.4}/PKG-INFO +6 -2
  2. {pgwidgets_python-0.3.2 → pgwidgets_python-0.3.4}/README.md +4 -0
  3. {pgwidgets_python-0.3.2 → pgwidgets_python-0.3.4}/docs/conf.py +2 -0
  4. {pgwidgets_python-0.3.2 → pgwidgets_python-0.3.4}/pgwidgets/extras/file_browser.py +4 -0
  5. pgwidgets_python-0.3.4/pgwidgets/icons/pgw-python-logo.png +0 -0
  6. {pgwidgets_python-0.3.2 → pgwidgets_python-0.3.4}/pgwidgets/method_types.py +2 -3
  7. {pgwidgets_python-0.3.2 → pgwidgets_python-0.3.4}/pgwidgets/sync/application.py +12 -0
  8. pgwidgets_python-0.3.4/pgwidgets/sync/text_source.py +200 -0
  9. {pgwidgets_python-0.3.2 → pgwidgets_python-0.3.4}/pgwidgets/sync/widget.py +5 -0
  10. pgwidgets_python-0.3.4/pgwidgets/text_model.py +792 -0
  11. {pgwidgets_python-0.3.2 → pgwidgets_python-0.3.4}/pgwidgets_python.egg-info/PKG-INFO +6 -2
  12. {pgwidgets_python-0.3.2 → pgwidgets_python-0.3.4}/pgwidgets_python.egg-info/SOURCES.txt +6 -0
  13. {pgwidgets_python-0.3.2 → pgwidgets_python-0.3.4}/pgwidgets_python.egg-info/requires.txt +1 -1
  14. pgwidgets_python-0.3.4/pgwidgets_python.egg-info/scm_file_list.json +71 -0
  15. pgwidgets_python-0.3.4/pgwidgets_python.egg-info/scm_version.json +8 -0
  16. {pgwidgets_python-0.3.2 → pgwidgets_python-0.3.4}/pyproject.toml +10 -5
  17. pgwidgets_python-0.3.4/tests/test_text_source.py +161 -0
  18. {pgwidgets_python-0.3.2 → pgwidgets_python-0.3.4}/.flake8 +0 -0
  19. {pgwidgets_python-0.3.2 → pgwidgets_python-0.3.4}/.github/workflows/tests.yml +0 -0
  20. {pgwidgets_python-0.3.2 → pgwidgets_python-0.3.4}/.gitignore +0 -0
  21. {pgwidgets_python-0.3.2 → pgwidgets_python-0.3.4}/.readthedocs.yaml +0 -0
  22. {pgwidgets_python-0.3.2 → pgwidgets_python-0.3.4}/LICENSE.md +0 -0
  23. {pgwidgets_python-0.3.2 → pgwidgets_python-0.3.4}/docs/Makefile +0 -0
  24. {pgwidgets_python-0.3.2 → pgwidgets_python-0.3.4}/docs/WhatsNew.rst +0 -0
  25. {pgwidgets_python-0.3.2 → pgwidgets_python-0.3.4}/docs/api/async.rst +0 -0
  26. {pgwidgets_python-0.3.2 → pgwidgets_python-0.3.4}/docs/api/index.rst +0 -0
  27. {pgwidgets_python-0.3.2 → pgwidgets_python-0.3.4}/docs/api/sync.rst +0 -0
  28. {pgwidgets_python-0.3.2 → pgwidgets_python-0.3.4}/docs/architecture.rst +0 -0
  29. {pgwidgets_python-0.3.2 → pgwidgets_python-0.3.4}/docs/async.rst +0 -0
  30. {pgwidgets_python-0.3.2 → pgwidgets_python-0.3.4}/docs/callbacks.rst +0 -0
  31. {pgwidgets_python-0.3.2 → pgwidgets_python-0.3.4}/docs/extras.rst +0 -0
  32. {pgwidgets_python-0.3.2 → pgwidgets_python-0.3.4}/docs/getting-started.rst +0 -0
  33. {pgwidgets_python-0.3.2 → pgwidgets_python-0.3.4}/docs/index.rst +0 -0
  34. {pgwidgets_python-0.3.2 → pgwidgets_python-0.3.4}/docs/subclassing.rst +0 -0
  35. {pgwidgets_python-0.3.2 → pgwidgets_python-0.3.4}/docs/sync.rst +0 -0
  36. {pgwidgets_python-0.3.2 → pgwidgets_python-0.3.4}/docs/utilities.rst +0 -0
  37. {pgwidgets_python-0.3.2 → pgwidgets_python-0.3.4}/docs/web-servers.rst +0 -0
  38. {pgwidgets_python-0.3.2 → pgwidgets_python-0.3.4}/docs/widgets.rst +0 -0
  39. {pgwidgets_python-0.3.2 → pgwidgets_python-0.3.4}/examples/README.md +0 -0
  40. {pgwidgets_python-0.3.2 → pgwidgets_python-0.3.4}/examples/all_widgets.py +0 -0
  41. {pgwidgets_python-0.3.2 → pgwidgets_python-0.3.4}/examples/all_widgets_async.py +0 -0
  42. {pgwidgets_python-0.3.2 → pgwidgets_python-0.3.4}/examples/demo_async.py +0 -0
  43. {pgwidgets_python-0.3.2 → pgwidgets_python-0.3.4}/examples/demo_sync.py +0 -0
  44. {pgwidgets_python-0.3.2 → pgwidgets_python-0.3.4}/examples/demo_treeview.py +0 -0
  45. {pgwidgets_python-0.3.2 → pgwidgets_python-0.3.4}/examples/flask-multi-process/README.md +0 -0
  46. {pgwidgets_python-0.3.2 → pgwidgets_python-0.3.4}/examples/flask-multi-process/gunicorn.conf.py +0 -0
  47. {pgwidgets_python-0.3.2 → pgwidgets_python-0.3.4}/examples/flask-multi-process/nginx.conf +0 -0
  48. {pgwidgets_python-0.3.2 → pgwidgets_python-0.3.4}/examples/flask-multi-process/server.py +0 -0
  49. {pgwidgets_python-0.3.2 → pgwidgets_python-0.3.4}/examples/flask-multi-process/user_app.py +0 -0
  50. {pgwidgets_python-0.3.2 → pgwidgets_python-0.3.4}/pgwidgets/__init__.py +0 -0
  51. {pgwidgets_python-0.3.2 → pgwidgets_python-0.3.4}/pgwidgets/_json.py +0 -0
  52. {pgwidgets_python-0.3.2 → pgwidgets_python-0.3.4}/pgwidgets/async_/Widgets.py +0 -0
  53. {pgwidgets_python-0.3.2 → pgwidgets_python-0.3.4}/pgwidgets/async_/__init__.py +0 -0
  54. {pgwidgets_python-0.3.2 → pgwidgets_python-0.3.4}/pgwidgets/async_/application.py +0 -0
  55. {pgwidgets_python-0.3.2 → pgwidgets_python-0.3.4}/pgwidgets/async_/widget.py +0 -0
  56. {pgwidgets_python-0.3.2 → pgwidgets_python-0.3.4}/pgwidgets/buffer.py +0 -0
  57. {pgwidgets_python-0.3.2 → pgwidgets_python-0.3.4}/pgwidgets/callbacks.py +0 -0
  58. {pgwidgets_python-0.3.2 → pgwidgets_python-0.3.4}/pgwidgets/defs.py +0 -0
  59. {pgwidgets_python-0.3.2 → pgwidgets_python-0.3.4}/pgwidgets/extras/__init__.py +0 -0
  60. {pgwidgets_python-0.3.2 → pgwidgets_python-0.3.4}/pgwidgets/sync/Widgets.py +0 -0
  61. {pgwidgets_python-0.3.2 → pgwidgets_python-0.3.4}/pgwidgets/sync/__init__.py +0 -0
  62. {pgwidgets_python-0.3.2 → pgwidgets_python-0.3.4}/pgwidgets_python.egg-info/dependency_links.txt +0 -0
  63. {pgwidgets_python-0.3.2 → pgwidgets_python-0.3.4}/pgwidgets_python.egg-info/top_level.txt +0 -0
  64. {pgwidgets_python-0.3.2 → pgwidgets_python-0.3.4}/setup.cfg +0 -0
  65. {pgwidgets_python-0.3.2 → pgwidgets_python-0.3.4}/test_pg2.py +0 -0
  66. {pgwidgets_python-0.3.2 → pgwidgets_python-0.3.4}/tests/__init__.py +0 -0
  67. {pgwidgets_python-0.3.2 → pgwidgets_python-0.3.4}/tests/test_async_session.py +0 -0
  68. {pgwidgets_python-0.3.2 → pgwidgets_python-0.3.4}/tests/test_defs.py +0 -0
  69. {pgwidgets_python-0.3.2 → pgwidgets_python-0.3.4}/tests/test_extras.py +0 -0
  70. {pgwidgets_python-0.3.2 → pgwidgets_python-0.3.4}/tests/test_fonts.py +0 -0
  71. {pgwidgets_python-0.3.2 → pgwidgets_python-0.3.4}/tests/test_protocol.py +0 -0
  72. {pgwidgets_python-0.3.2 → pgwidgets_python-0.3.4}/tests/test_reconstruct.py +0 -0
  73. {pgwidgets_python-0.3.2 → pgwidgets_python-0.3.4}/tests/test_resolve_kwargs.py +0 -0
  74. {pgwidgets_python-0.3.2 → pgwidgets_python-0.3.4}/tests/test_session.py +0 -0
  75. {pgwidgets_python-0.3.2 → pgwidgets_python-0.3.4}/tests/test_stateful.py +0 -0
  76. {pgwidgets_python-0.3.2 → pgwidgets_python-0.3.4}/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.2
3
+ Version: 0.3.4
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"
@@ -27,6 +27,10 @@ Provides-Extra: test
27
27
  Requires-Dist: pytest; extra == "test"
28
28
  Dynamic: license-file
29
29
 
30
+ <p align="center">
31
+ <img src="pgwidgets/icons/pgw-python-logo.png" alt="pgwidgets-python logo" width="500">
32
+ </p>
33
+
30
34
  # pgwidgets — Python Bindings
31
35
 
32
36
  Python bindings for the [pgwidgets](https://github.com/naojsoft/pgwidgets-js)
@@ -1,3 +1,7 @@
1
+ <p align="center">
2
+ <img src="pgwidgets/icons/pgw-python-logo.png" alt="pgwidgets-python logo" width="500">
3
+ </p>
4
+
1
5
  # pgwidgets — Python Bindings
2
6
 
3
7
  Python bindings for the [pgwidgets](https://github.com/naojsoft/pgwidgets-js)
@@ -17,6 +17,8 @@ exclude_patterns = ["_build"]
17
17
  # -- HTML output --
18
18
  html_theme = "furo"
19
19
  html_title = "pgwidgets-python"
20
+ # project logo (single source: the package's icons/ data folder)
21
+ html_logo = "../pgwidgets/icons/pgw-python-logo.png"
20
22
 
21
23
  # -- autodoc --
22
24
  autodoc_member_order = "bysource"
@@ -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
 
@@ -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.
@@ -1399,6 +1399,18 @@ class Session:
1399
1399
  # callbacks transferred by _transfer_proxy during step 3.
1400
1400
  self._reregister_callbacks(widget)
1401
1401
 
1402
+ # 6. Widgets with a Python-authoritative model (TextSource) push
1403
+ # their full model state (text, tags, cursor) to the freshly
1404
+ # (re)connected browser. Guard it: a failure here must not abort
1405
+ # reconstruction of the whole widget tree.
1406
+ if hasattr(widget, "_reconstruct_model"):
1407
+ try:
1408
+ widget._reconstruct_model()
1409
+ except Exception:
1410
+ self._logger.error(
1411
+ "TextSource model reconstruction failed for wid=%s",
1412
+ getattr(widget, "_wid", "?"), exc_info=True)
1413
+
1402
1414
  def _reregister_callbacks(self, widget):
1403
1415
  """Re-register user callbacks and auto-sync listeners on *widget*.
1404
1416
 
@@ -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