pgwidgets-python 0.3.1__tar.gz → 0.3.3__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 (73) hide show
  1. {pgwidgets_python-0.3.1 → pgwidgets_python-0.3.3}/PKG-INFO +5 -1
  2. {pgwidgets_python-0.3.1 → pgwidgets_python-0.3.3}/README.md +4 -0
  3. {pgwidgets_python-0.3.1 → pgwidgets_python-0.3.3}/docs/WhatsNew.rst +34 -0
  4. {pgwidgets_python-0.3.1 → pgwidgets_python-0.3.3}/docs/async.rst +4 -0
  5. {pgwidgets_python-0.3.1 → pgwidgets_python-0.3.3}/docs/conf.py +2 -0
  6. {pgwidgets_python-0.3.1 → pgwidgets_python-0.3.3}/docs/sync.rst +13 -0
  7. {pgwidgets_python-0.3.1 → pgwidgets_python-0.3.3}/docs/widgets.rst +6 -0
  8. {pgwidgets_python-0.3.1 → pgwidgets_python-0.3.3}/pgwidgets/async_/application.py +18 -1
  9. pgwidgets_python-0.3.3/pgwidgets/icons/pgw-python-logo.png +0 -0
  10. {pgwidgets_python-0.3.1 → pgwidgets_python-0.3.3}/pgwidgets/method_types.py +1 -1
  11. {pgwidgets_python-0.3.1 → pgwidgets_python-0.3.3}/pgwidgets/sync/application.py +41 -8
  12. {pgwidgets_python-0.3.1 → pgwidgets_python-0.3.3}/pgwidgets_python.egg-info/PKG-INFO +5 -1
  13. {pgwidgets_python-0.3.1 → pgwidgets_python-0.3.3}/pgwidgets_python.egg-info/SOURCES.txt +4 -0
  14. pgwidgets_python-0.3.3/pgwidgets_python.egg-info/scm_file_list.json +68 -0
  15. pgwidgets_python-0.3.3/pgwidgets_python.egg-info/scm_version.json +8 -0
  16. {pgwidgets_python-0.3.1 → pgwidgets_python-0.3.3}/pyproject.toml +3 -0
  17. pgwidgets_python-0.3.3/tests/test_async_session.py +68 -0
  18. {pgwidgets_python-0.3.1 → pgwidgets_python-0.3.3}/tests/test_session.py +50 -0
  19. {pgwidgets_python-0.3.1 → pgwidgets_python-0.3.3}/.flake8 +0 -0
  20. {pgwidgets_python-0.3.1 → pgwidgets_python-0.3.3}/.github/workflows/tests.yml +0 -0
  21. {pgwidgets_python-0.3.1 → pgwidgets_python-0.3.3}/.gitignore +0 -0
  22. {pgwidgets_python-0.3.1 → pgwidgets_python-0.3.3}/.readthedocs.yaml +0 -0
  23. {pgwidgets_python-0.3.1 → pgwidgets_python-0.3.3}/LICENSE.md +0 -0
  24. {pgwidgets_python-0.3.1 → pgwidgets_python-0.3.3}/docs/Makefile +0 -0
  25. {pgwidgets_python-0.3.1 → pgwidgets_python-0.3.3}/docs/api/async.rst +0 -0
  26. {pgwidgets_python-0.3.1 → pgwidgets_python-0.3.3}/docs/api/index.rst +0 -0
  27. {pgwidgets_python-0.3.1 → pgwidgets_python-0.3.3}/docs/api/sync.rst +0 -0
  28. {pgwidgets_python-0.3.1 → pgwidgets_python-0.3.3}/docs/architecture.rst +0 -0
  29. {pgwidgets_python-0.3.1 → pgwidgets_python-0.3.3}/docs/callbacks.rst +0 -0
  30. {pgwidgets_python-0.3.1 → pgwidgets_python-0.3.3}/docs/extras.rst +0 -0
  31. {pgwidgets_python-0.3.1 → pgwidgets_python-0.3.3}/docs/getting-started.rst +0 -0
  32. {pgwidgets_python-0.3.1 → pgwidgets_python-0.3.3}/docs/index.rst +0 -0
  33. {pgwidgets_python-0.3.1 → pgwidgets_python-0.3.3}/docs/subclassing.rst +0 -0
  34. {pgwidgets_python-0.3.1 → pgwidgets_python-0.3.3}/docs/utilities.rst +0 -0
  35. {pgwidgets_python-0.3.1 → pgwidgets_python-0.3.3}/docs/web-servers.rst +0 -0
  36. {pgwidgets_python-0.3.1 → pgwidgets_python-0.3.3}/examples/README.md +0 -0
  37. {pgwidgets_python-0.3.1 → pgwidgets_python-0.3.3}/examples/all_widgets.py +0 -0
  38. {pgwidgets_python-0.3.1 → pgwidgets_python-0.3.3}/examples/all_widgets_async.py +0 -0
  39. {pgwidgets_python-0.3.1 → pgwidgets_python-0.3.3}/examples/demo_async.py +0 -0
  40. {pgwidgets_python-0.3.1 → pgwidgets_python-0.3.3}/examples/demo_sync.py +0 -0
  41. {pgwidgets_python-0.3.1 → pgwidgets_python-0.3.3}/examples/demo_treeview.py +0 -0
  42. {pgwidgets_python-0.3.1 → pgwidgets_python-0.3.3}/examples/flask-multi-process/README.md +0 -0
  43. {pgwidgets_python-0.3.1 → pgwidgets_python-0.3.3}/examples/flask-multi-process/gunicorn.conf.py +0 -0
  44. {pgwidgets_python-0.3.1 → pgwidgets_python-0.3.3}/examples/flask-multi-process/nginx.conf +0 -0
  45. {pgwidgets_python-0.3.1 → pgwidgets_python-0.3.3}/examples/flask-multi-process/server.py +0 -0
  46. {pgwidgets_python-0.3.1 → pgwidgets_python-0.3.3}/examples/flask-multi-process/user_app.py +0 -0
  47. {pgwidgets_python-0.3.1 → pgwidgets_python-0.3.3}/pgwidgets/__init__.py +0 -0
  48. {pgwidgets_python-0.3.1 → pgwidgets_python-0.3.3}/pgwidgets/_json.py +0 -0
  49. {pgwidgets_python-0.3.1 → pgwidgets_python-0.3.3}/pgwidgets/async_/Widgets.py +0 -0
  50. {pgwidgets_python-0.3.1 → pgwidgets_python-0.3.3}/pgwidgets/async_/__init__.py +0 -0
  51. {pgwidgets_python-0.3.1 → pgwidgets_python-0.3.3}/pgwidgets/async_/widget.py +0 -0
  52. {pgwidgets_python-0.3.1 → pgwidgets_python-0.3.3}/pgwidgets/buffer.py +0 -0
  53. {pgwidgets_python-0.3.1 → pgwidgets_python-0.3.3}/pgwidgets/callbacks.py +0 -0
  54. {pgwidgets_python-0.3.1 → pgwidgets_python-0.3.3}/pgwidgets/defs.py +0 -0
  55. {pgwidgets_python-0.3.1 → pgwidgets_python-0.3.3}/pgwidgets/extras/__init__.py +0 -0
  56. {pgwidgets_python-0.3.1 → pgwidgets_python-0.3.3}/pgwidgets/extras/file_browser.py +0 -0
  57. {pgwidgets_python-0.3.1 → pgwidgets_python-0.3.3}/pgwidgets/sync/Widgets.py +0 -0
  58. {pgwidgets_python-0.3.1 → pgwidgets_python-0.3.3}/pgwidgets/sync/__init__.py +0 -0
  59. {pgwidgets_python-0.3.1 → pgwidgets_python-0.3.3}/pgwidgets/sync/widget.py +0 -0
  60. {pgwidgets_python-0.3.1 → pgwidgets_python-0.3.3}/pgwidgets_python.egg-info/dependency_links.txt +0 -0
  61. {pgwidgets_python-0.3.1 → pgwidgets_python-0.3.3}/pgwidgets_python.egg-info/requires.txt +0 -0
  62. {pgwidgets_python-0.3.1 → pgwidgets_python-0.3.3}/pgwidgets_python.egg-info/top_level.txt +0 -0
  63. {pgwidgets_python-0.3.1 → pgwidgets_python-0.3.3}/setup.cfg +0 -0
  64. {pgwidgets_python-0.3.1 → pgwidgets_python-0.3.3}/test_pg2.py +0 -0
  65. {pgwidgets_python-0.3.1 → pgwidgets_python-0.3.3}/tests/__init__.py +0 -0
  66. {pgwidgets_python-0.3.1 → pgwidgets_python-0.3.3}/tests/test_defs.py +0 -0
  67. {pgwidgets_python-0.3.1 → pgwidgets_python-0.3.3}/tests/test_extras.py +0 -0
  68. {pgwidgets_python-0.3.1 → pgwidgets_python-0.3.3}/tests/test_fonts.py +0 -0
  69. {pgwidgets_python-0.3.1 → pgwidgets_python-0.3.3}/tests/test_protocol.py +0 -0
  70. {pgwidgets_python-0.3.1 → pgwidgets_python-0.3.3}/tests/test_reconstruct.py +0 -0
  71. {pgwidgets_python-0.3.1 → pgwidgets_python-0.3.3}/tests/test_resolve_kwargs.py +0 -0
  72. {pgwidgets_python-0.3.1 → pgwidgets_python-0.3.3}/tests/test_stateful.py +0 -0
  73. {pgwidgets_python-0.3.1 → pgwidgets_python-0.3.3}/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.1
3
+ Version: 0.3.3
4
4
  Summary: Python bindings for the pgwidgets JavaScript widget library
5
5
  Author: PGWidgets Developers
6
6
  License: BSD-3-Clause
@@ -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)
@@ -4,6 +4,40 @@ What's New
4
4
  Recent changes — since ``v0.3.0``
5
5
  ---------------------------------
6
6
 
7
+ ``Application.open_url``
8
+ ~~~~~~~~~~~~~~~~~~~~~~~~~
9
+
10
+ A new method asks the connected browser(s) to open a URL in a new
11
+ tab/window (``window.open``):
12
+
13
+ .. code-block:: python
14
+
15
+ app.open_url('https://example.com/')
16
+
17
+ This opens the link in the *user's* browser rather than on the host
18
+ running Python. It is a fire-and-forget, per-session broadcast (like
19
+ ``set_default_font``) and is available on both the sync and async
20
+ ``Application``. (Note: a browser popup blocker may suppress it if
21
+ the originating user-activation has lapsed.)
22
+
23
+ Containers report their children: ``get_children``
24
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
25
+
26
+ ``Frame``, ``Expander``, ``TopLevel`` and ``Page`` (single-child
27
+ containers) and the menu / toolbar widgets ``Menu``, ``MenuBar`` and
28
+ ``ToolBar`` are now container widgets, so their generated wrappers
29
+ expose the container method set -- ``get_children`` / ``num_children``
30
+ / ``remove`` / ``remove_widget`` / ``remove_all``. ``get_children()``
31
+ returns the contained widgets (a 0- or 1-element list for the
32
+ single-child containers; the menu actions / submenus / toolbar items
33
+ for the menu/toolbar widgets).
34
+
35
+ TreeView / TableView: ``set_row_spacing`` / ``set_column_spacing``
36
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
37
+
38
+ Two new methods set the vertical (row) and horizontal (column) cell
39
+ padding (in pixels). ``set_row_spacing(0)`` yields tight rows.
40
+
7
41
  Custom fonts: ``Application.register_font`` / ``set_default_font``
8
42
  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
9
43
 
@@ -79,6 +79,10 @@ are replayed to every new and reconnecting session before
79
79
  with ``set_font(family, ...)`` always find the face already
80
80
  declared.
81
81
 
82
+ ``open_url(url)`` is also available (same as the sync backend; see
83
+ :doc:`sync`) -- it asks the connected browser(s) to open ``url`` in a
84
+ new tab via ``window.open``.
85
+
82
86
  Session
83
87
  -------
84
88
 
@@ -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"
@@ -117,6 +117,19 @@ nginx the same route is served from the Python process.
117
117
  See also the JS-side write-up at :ref:`custom-fonts` in the
118
118
  pgwidgets-js documentation.
119
119
 
120
+ Opening a URL in the browser
121
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
122
+
123
+ .. code-block:: python
124
+
125
+ app.open_url('https://example.com/')
126
+
127
+ ``open_url(url)`` asks the connected browser(s) to open ``url`` in a
128
+ new tab/window (``window.open``) -- i.e. in the *user's* browser, not
129
+ on the host running Python. It is a fire-and-forget, per-session
130
+ broadcast. (A browser popup blocker may suppress it if the
131
+ user-activation from the originating click has already lapsed.)
132
+
120
133
  Session
121
134
  -------
122
135
 
@@ -631,6 +631,7 @@ Hierarchical tree/list display.
631
631
  ``select_all(state)``,
632
632
  ``set_column_width(col_key, width)``,
633
633
  ``set_optimal_column_widths()``,
634
+ ``set_row_spacing(px)``, ``set_column_spacing(px)``,
634
635
  ``sort_by_column(col_key, ascending)``, ``scroll_to_path(path)``,
635
636
  ``scroll_to_end()``, ``get_column_count()``, ``get_row_count()``,
636
637
  ``set_show_grid(tf)``, ``set_show_row_numbers(tf)``,
@@ -815,6 +816,11 @@ Browser file open/save dialog.
815
816
  Menus and Toolbars
816
817
  ------------------
817
818
 
819
+ ``MenuBar``, ``Menu`` and ``ToolBar`` are container widgets: the menu
820
+ actions, submenus and toolbar items they hold are reported by
821
+ ``get_children()`` / ``num_children()`` (separators and spacers
822
+ excluded).
823
+
818
824
  MenuBar
819
825
  ~~~~~~~
820
826
 
@@ -1557,6 +1557,15 @@ class Application:
1557
1557
  }
1558
1558
  self._broadcast_font_msg(self._font_default_msg())
1559
1559
 
1560
+ def open_url(self, url):
1561
+ """Ask the connected browser(s) to open *url* in a new tab/window.
1562
+
1563
+ Fire-and-forget app-level command; opens the link in the *user's*
1564
+ browser rather than on the host running Python. (Reuses the
1565
+ generic per-session broadcast helper.)
1566
+ """
1567
+ self._broadcast_font_msg({"type": "open-url", "url": str(url)})
1568
+
1560
1569
  def _font_register_msg(self, entry):
1561
1570
  return {
1562
1571
  "type": "register-font",
@@ -1788,8 +1797,14 @@ class Application:
1788
1797
  The newly created session.
1789
1798
  """
1790
1799
  if session_id is None:
1800
+ # skip ids already taken by explicitly-created sessions
1801
+ # (e.g. a pre-created default session) so we never
1802
+ # silently overwrite an existing session
1791
1803
  session_id = self._next_session_id
1792
1804
  self._next_session_id += 1
1805
+ while session_id in self._sessions:
1806
+ session_id = self._next_session_id
1807
+ self._next_session_id += 1
1793
1808
  elif session_id in self._sessions:
1794
1809
  # If the existing session was auto-created by a browser
1795
1810
  # reconnect with the same token, return it so the user's
@@ -1954,12 +1969,14 @@ class Application:
1954
1969
 
1955
1970
  In the async API, callbacks are dispatched by the asyncio event
1956
1971
  loop directly, so this simply yields control for the requested
1957
- duration.
1972
+ duration. A *timeout* of ``0`` yields once (a non-blocking drain
1973
+ of ready callbacks), matching the sync API's contract.
1958
1974
 
1959
1975
  Parameters
1960
1976
  ----------
1961
1977
  timeout : float
1962
1978
  Seconds to yield to the event loop (default 0.1).
1979
+ Use ``0`` for a single non-blocking yield.
1963
1980
  """
1964
1981
  await asyncio.sleep(timeout)
1965
1982
 
@@ -56,7 +56,7 @@ JS_ONLY_METHODS = {
56
56
  # ComboBox
57
57
  "get_alpha",
58
58
  # Container
59
- "num_children",
59
+ "num_children", "is_container",
60
60
  # TabWidget lookups
61
61
  "get_tab_id", "get_child", "index_of",
62
62
  # MDIWidget
@@ -1828,6 +1828,20 @@ class Application:
1828
1828
  except Exception:
1829
1829
  pass
1830
1830
 
1831
+ def open_url(self, url):
1832
+ """Ask the connected browser(s) to open *url* in a new tab/window.
1833
+
1834
+ Fire-and-forget app-level command (mirrors set_default_font's
1835
+ per-session send). This opens the link in the *user's* browser
1836
+ rather than on the host running Python.
1837
+ """
1838
+ msg = {"type": "open-url", "url": str(url)}
1839
+ for session in list(self._sessions.values()):
1840
+ try:
1841
+ session._send(msg)
1842
+ except Exception:
1843
+ pass
1844
+
1831
1845
  def _font_register_msg(self, entry):
1832
1846
  return {
1833
1847
  "type": "register-font",
@@ -2062,8 +2076,14 @@ class Application:
2062
2076
  """
2063
2077
  with self._session_lock:
2064
2078
  if session_id is None:
2079
+ # skip ids already taken by explicitly-created sessions
2080
+ # (e.g. a pre-created default session) so we never
2081
+ # silently overwrite an existing session
2065
2082
  session_id = self._next_session_id
2066
2083
  self._next_session_id += 1
2084
+ while session_id in self._sessions:
2085
+ session_id = self._next_session_id
2086
+ self._next_session_id += 1
2067
2087
  elif session_id in self._sessions:
2068
2088
  # If the existing session was auto-created by a browser
2069
2089
  # reconnect with the same token, return it so the user's
@@ -2279,22 +2299,35 @@ class Application:
2279
2299
  ``concurrent`` modes, callbacks already run on their own threads
2280
2300
  so this method simply yields control briefly.
2281
2301
 
2302
+ A *timeout* of ``0`` drains every callback currently queued and
2303
+ returns immediately without blocking -- useful when pumping from
2304
+ within another event loop (e.g. a Jupyter kernel's asyncio loop).
2305
+
2282
2306
  Parameters
2283
2307
  ----------
2284
2308
  timeout : float
2285
2309
  Maximum seconds to spend processing events (default 0.1).
2310
+ Use ``0`` for a non-blocking drain of the pending queue.
2286
2311
  """
2287
2312
  if self._concurrency == "serialized":
2313
+ non_blocking = (timeout == 0)
2288
2314
  deadline = time.monotonic() + timeout
2289
2315
  while not self._shutdown.is_set():
2290
- remaining = deadline - time.monotonic()
2291
- if remaining <= 0:
2292
- break
2293
- try:
2294
- handler, args, kwargs, result_slot = \
2295
- self._cb_queue.get(timeout=min(remaining, 0.5))
2296
- except queue.Empty:
2297
- continue
2316
+ if non_blocking:
2317
+ try:
2318
+ handler, args, kwargs, result_slot = \
2319
+ self._cb_queue.get_nowait()
2320
+ except queue.Empty:
2321
+ break
2322
+ else:
2323
+ remaining = deadline - time.monotonic()
2324
+ if remaining <= 0:
2325
+ break
2326
+ try:
2327
+ handler, args, kwargs, result_slot = \
2328
+ self._cb_queue.get(timeout=min(remaining, 0.5))
2329
+ except queue.Empty:
2330
+ continue
2298
2331
  try:
2299
2332
  rv = handler(*args, **kwargs)
2300
2333
  if result_slot is not None:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pgwidgets-python
3
- Version: 0.3.1
3
+ Version: 0.3.3
4
4
  Summary: Python bindings for the pgwidgets JavaScript widget library
5
5
  Author: PGWidgets Developers
6
6
  License: BSD-3-Clause
@@ -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)
@@ -46,6 +46,7 @@ pgwidgets/async_/application.py
46
46
  pgwidgets/async_/widget.py
47
47
  pgwidgets/extras/__init__.py
48
48
  pgwidgets/extras/file_browser.py
49
+ pgwidgets/icons/pgw-python-logo.png
49
50
  pgwidgets/sync/Widgets.py
50
51
  pgwidgets/sync/__init__.py
51
52
  pgwidgets/sync/application.py
@@ -54,8 +55,11 @@ pgwidgets_python.egg-info/PKG-INFO
54
55
  pgwidgets_python.egg-info/SOURCES.txt
55
56
  pgwidgets_python.egg-info/dependency_links.txt
56
57
  pgwidgets_python.egg-info/requires.txt
58
+ pgwidgets_python.egg-info/scm_file_list.json
59
+ pgwidgets_python.egg-info/scm_version.json
57
60
  pgwidgets_python.egg-info/top_level.txt
58
61
  tests/__init__.py
62
+ tests/test_async_session.py
59
63
  tests/test_defs.py
60
64
  tests/test_extras.py
61
65
  tests/test_fonts.py
@@ -0,0 +1,68 @@
1
+ {
2
+ "files": [
3
+ ".flake8",
4
+ "test_pg2.py",
5
+ "README.md",
6
+ "pyproject.toml",
7
+ "LICENSE.md",
8
+ ".readthedocs.yaml",
9
+ ".gitignore",
10
+ "docs/web-servers.rst",
11
+ "docs/WhatsNew.rst",
12
+ "docs/subclassing.rst",
13
+ "docs/sync.rst",
14
+ "docs/utilities.rst",
15
+ "docs/extras.rst",
16
+ "docs/async.rst",
17
+ "docs/architecture.rst",
18
+ "docs/widgets.rst",
19
+ "docs/conf.py",
20
+ "docs/Makefile",
21
+ "docs/index.rst",
22
+ "docs/callbacks.rst",
23
+ "docs/getting-started.rst",
24
+ "docs/api/async.rst",
25
+ "docs/api/sync.rst",
26
+ "docs/api/index.rst",
27
+ "tests/test_protocol.py",
28
+ "tests/__init__.py",
29
+ "tests/test_stateful.py",
30
+ "tests/test_resolve_kwargs.py",
31
+ "tests/test_async_session.py",
32
+ "tests/test_defs.py",
33
+ "tests/test_fonts.py",
34
+ "tests/test_session.py",
35
+ "tests/test_extras.py",
36
+ "tests/test_widget_classes.py",
37
+ "tests/test_reconstruct.py",
38
+ "pgwidgets/_json.py",
39
+ "pgwidgets/__init__.py",
40
+ "pgwidgets/callbacks.py",
41
+ "pgwidgets/method_types.py",
42
+ "pgwidgets/buffer.py",
43
+ "pgwidgets/defs.py",
44
+ "pgwidgets/sync/Widgets.py",
45
+ "pgwidgets/sync/__init__.py",
46
+ "pgwidgets/sync/application.py",
47
+ "pgwidgets/sync/widget.py",
48
+ "pgwidgets/extras/__init__.py",
49
+ "pgwidgets/extras/file_browser.py",
50
+ "pgwidgets/icons/pgw-python-logo.png",
51
+ "pgwidgets/async_/Widgets.py",
52
+ "pgwidgets/async_/application.py",
53
+ "pgwidgets/async_/__init__.py",
54
+ "pgwidgets/async_/widget.py",
55
+ "examples/demo_sync.py",
56
+ "examples/all_widgets.py",
57
+ "examples/all_widgets_async.py",
58
+ "examples/README.md",
59
+ "examples/demo_treeview.py",
60
+ "examples/demo_async.py",
61
+ "examples/flask-multi-process/server.py",
62
+ "examples/flask-multi-process/README.md",
63
+ "examples/flask-multi-process/nginx.conf",
64
+ "examples/flask-multi-process/gunicorn.conf.py",
65
+ "examples/flask-multi-process/user_app.py",
66
+ ".github/workflows/tests.yml"
67
+ ]
68
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "tag": "0.3.3",
3
+ "distance": 0,
4
+ "node": "g522f05596d51c6b3f6f671895e54ec014515d3c7",
5
+ "dirty": false,
6
+ "branch": "main",
7
+ "node_date": "2026-06-25"
8
+ }
@@ -46,3 +46,6 @@ local_scheme = "node-and-date"
46
46
 
47
47
  [tool.setuptools.packages.find]
48
48
  include = ["pgwidgets*"]
49
+
50
+ [tool.setuptools.package-data]
51
+ pgwidgets = ["icons/*"]
@@ -0,0 +1,68 @@
1
+ """
2
+ Tests for the async backend's session lifecycle.
3
+
4
+ ``create_session`` is a plain (synchronous) method, so these tests need
5
+ no running event loop. They mirror the auto-id / collision-safety tests
6
+ of the sync backend in ``test_session.py``.
7
+ """
8
+
9
+ import logging
10
+ import threading
11
+
12
+ from pgwidgets.async_.application import Application
13
+
14
+
15
+ def _make_app(**kwargs):
16
+ """Create an async Application without starting servers."""
17
+ app = Application.__new__(Application)
18
+ app._host = "127.0.0.1"
19
+ app._ws_port = 9500
20
+ app._http_port = 9501
21
+ app._use_http_server = False
22
+ app._concurrency = kwargs.get("concurrency", "concurrent")
23
+ app._max_sessions = None
24
+ app._sessions = {}
25
+ app._next_session_id = 1
26
+ app._on_connect = None
27
+ app._on_disconnect = None
28
+ app._widget_classes = {}
29
+
30
+ logger = logging.getLogger("pgwidgets.test.async")
31
+ logger.addHandler(logging.NullHandler())
32
+ app._logger = logger
33
+
34
+ return app
35
+
36
+
37
+ class TestAsyncSessionCreation:
38
+ def test_create_session_auto_id(self):
39
+ app = _make_app()
40
+ s1 = app.create_session()
41
+ s2 = app.create_session()
42
+ assert s1.id == 1
43
+ assert s2.id == 2
44
+
45
+ def test_create_session_auto_id_skips_existing(self):
46
+ # A default session is created with an explicit id, then more are
47
+ # auto-allocated. An auto-allocated id must not collide with /
48
+ # silently overwrite the explicitly-created one.
49
+ app = _make_app()
50
+ default = app.create_session(session_id=1)
51
+ extra = app.create_session() # auto-allocated
52
+ assert extra.id != default.id
53
+ assert app._sessions[default.id] is default # not overwritten
54
+ assert app._sessions[extra.id] is extra
55
+
56
+ def test_create_session_explicit_id(self):
57
+ app = _make_app()
58
+ s = app.create_session(session_id="my-session")
59
+ assert s.id == "my-session"
60
+
61
+ def test_create_session_duplicate_id_raises(self):
62
+ app = _make_app()
63
+ app.create_session(session_id="dup")
64
+ try:
65
+ app.create_session(session_id="dup")
66
+ assert False, "Should have raised ValueError"
67
+ except ValueError as e:
68
+ assert "dup" in str(e)
@@ -6,7 +6,9 @@ tokens, manage multiple connections, handle graceful no-connection
6
6
  operation, and can be destroyed.
7
7
  """
8
8
 
9
+ import queue
9
10
  import threading
11
+ import time
10
12
  from unittest.mock import MagicMock, AsyncMock
11
13
 
12
14
  from pgwidgets.sync.application import Application, Session
@@ -59,6 +61,18 @@ class TestSessionCreation:
59
61
  assert s1.id == 1
60
62
  assert s2.id == 2
61
63
 
64
+ def test_create_session_auto_id_skips_existing(self):
65
+ # Mirrors the Ginga pgw Application pattern: a default session is
66
+ # created with an explicit id, then more are auto-allocated. An
67
+ # auto-allocated id must not collide with / silently overwrite the
68
+ # explicitly-created one.
69
+ app = _make_app()
70
+ default = app.create_session(session_id=1)
71
+ extra = app.create_session() # auto-allocated
72
+ assert extra.id != default.id
73
+ assert app._sessions[default.id] is default # not overwritten
74
+ assert app._sessions[extra.id] is extra
75
+
62
76
  def test_create_session_explicit_id(self):
63
77
  app = _make_app()
64
78
  s = app.create_session(session_id="my-session")
@@ -375,3 +389,39 @@ class TestCallbackSuppression:
375
389
  types = [m["type"] for m in messages]
376
390
  assert types[0] == "reconstruct-start"
377
391
  assert types[-1] == "reconstruct-end"
392
+
393
+
394
+ class TestProcessEventsDrain:
395
+ """Test the non-blocking drain mode of process_events (timeout=0)."""
396
+
397
+ def _serial_app(self):
398
+ app = _make_app(concurrency="serialized")
399
+ app._cb_queue = queue.Queue()
400
+ app._shutdown = threading.Event()
401
+ return app
402
+
403
+ def test_zero_drains_all_without_blocking(self):
404
+ app = self._serial_app()
405
+ ran = []
406
+ for i in range(3):
407
+ app._cb_queue.put((lambda i=i: ran.append(i), (), {}, None))
408
+ start = time.monotonic()
409
+ app.process_events(0)
410
+ elapsed = time.monotonic() - start
411
+ assert ran == [0, 1, 2]
412
+ assert app._cb_queue.empty()
413
+ assert elapsed < 0.05 # returned without blocking
414
+
415
+ def test_zero_empty_queue_returns_immediately(self):
416
+ app = self._serial_app()
417
+ start = time.monotonic()
418
+ app.process_events(0) # nothing queued
419
+ assert time.monotonic() - start < 0.05
420
+
421
+ def test_zero_propagates_result_slot(self):
422
+ app = self._serial_app()
423
+ slot = {"event": threading.Event()}
424
+ app._cb_queue.put((lambda: 42, (), {}, slot))
425
+ app.process_events(0)
426
+ assert slot["value"] == 42
427
+ assert slot["event"].is_set()