pgwidgets-python 0.3.0__py3-none-any.whl → 0.3.2__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.
@@ -16,6 +16,7 @@ import logging
16
16
  import mimetypes
17
17
  import signal
18
18
  import secrets
19
+ import threading
19
20
  import traceback
20
21
  from http.server import SimpleHTTPRequestHandler
21
22
  from pathlib import Path
@@ -37,6 +38,15 @@ from pgwidgets.async_.widget import Widget, build_all_widget_classes
37
38
  _CONCURRENCY_MODES = ("serialized", "per_session", "concurrent")
38
39
 
39
40
 
41
+ # MIME types for the font formats register_font accepts.
42
+ _FONT_MIME = {
43
+ ".ttf": "font/ttf",
44
+ ".otf": "font/otf",
45
+ ".woff": "font/woff",
46
+ ".woff2": "font/woff2",
47
+ }
48
+
49
+
40
50
  class _Namespace:
41
51
  """Holds widget factory methods as attributes (W.Button, W.Label, etc.)."""
42
52
  pass
@@ -1428,6 +1438,13 @@ class Application:
1428
1438
  self._session_semaphore = None # initialized in start()
1429
1439
  self._cb_lock = None # for "serialized" mode
1430
1440
 
1441
+ # Custom-font registry — see sync.application for details.
1442
+ self._fonts = []
1443
+ self._fonts_by_id = {}
1444
+ self._next_font_id = 1
1445
+ self._default_font = None
1446
+ self._font_lock = threading.Lock()
1447
+
1431
1448
  self._run_future = None # set in run(), cancelled by close()
1432
1449
  self._httpd = None # HTTP server instance
1433
1450
 
@@ -1493,6 +1510,113 @@ class Application:
1493
1510
  self._widget_classes[name] = cls
1494
1511
  return cls
1495
1512
 
1513
+ # ----- Custom font registration ---------------------------
1514
+ #
1515
+ # API matches the sync backend; see ``sync.application`` for
1516
+ # full docstrings. The async variant schedules sends on the
1517
+ # event loop via ``asyncio.run_coroutine_threadsafe`` so the
1518
+ # method is safe to call from outside the loop (e.g. from the
1519
+ # ``on_connect`` callback running on a worker thread).
1520
+
1521
+ def register_font(self, family, source, *,
1522
+ weight="normal", style="normal"):
1523
+ if isinstance(source, (bytes, bytearray, memoryview)):
1524
+ data = bytes(source)
1525
+ mime = "font/ttf"
1526
+ else:
1527
+ p = Path(source)
1528
+ data = p.read_bytes()
1529
+ mime = _FONT_MIME.get(p.suffix.lower(), "font/ttf")
1530
+ with self._font_lock:
1531
+ font_id = self._next_font_id
1532
+ self._next_font_id += 1
1533
+ entry = {
1534
+ "id": font_id,
1535
+ "family": str(family),
1536
+ "weight": str(weight),
1537
+ "style": str(style),
1538
+ "bytes": data,
1539
+ "mime": mime,
1540
+ }
1541
+ self._fonts.append(entry)
1542
+ self._fonts_by_id[font_id] = entry
1543
+ msg = self._font_register_msg(entry)
1544
+ self._broadcast_font_msg(msg)
1545
+ return font_id
1546
+
1547
+ def set_default_font(self, family, *, size=None,
1548
+ weight=None, style=None):
1549
+ if family is None:
1550
+ self._default_font = None
1551
+ else:
1552
+ self._default_font = {
1553
+ "family": str(family),
1554
+ "size": None if size is None else float(size),
1555
+ "weight": None if weight is None else str(weight),
1556
+ "style": None if style is None else str(style),
1557
+ }
1558
+ self._broadcast_font_msg(self._font_default_msg())
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
+
1569
+ def _font_register_msg(self, entry):
1570
+ return {
1571
+ "type": "register-font",
1572
+ "id": entry["id"],
1573
+ "family": entry["family"],
1574
+ "weight": entry["weight"],
1575
+ "style": entry["style"],
1576
+ "url": f"/_pgwidgets/font/{entry['id']}",
1577
+ }
1578
+
1579
+ def _font_default_msg(self):
1580
+ return {
1581
+ "type": "set-default-font",
1582
+ "font": self._default_font,
1583
+ }
1584
+
1585
+ def _broadcast_font_msg(self, msg):
1586
+ loop = getattr(self, "_loop", None) or asyncio.get_event_loop()
1587
+ for session in list(self._sessions.values()):
1588
+ try:
1589
+ asyncio.run_coroutine_threadsafe(
1590
+ session._send(dict(msg)), loop)
1591
+ except Exception:
1592
+ pass
1593
+
1594
+ async def _replay_fonts_to_session(self, session):
1595
+ """Push the registry + default font to a session before
1596
+ any user code runs. ``await``-ed from ``_on_session_open``
1597
+ so the JS side has loaded faces (or at least dispatched
1598
+ the load) before reconstruct / on_connect fires."""
1599
+ with self._font_lock:
1600
+ fonts = list(self._fonts)
1601
+ default = self._default_font
1602
+ for entry in fonts:
1603
+ try:
1604
+ await session._send(self._font_register_msg(entry))
1605
+ except Exception:
1606
+ pass
1607
+ if default is not None:
1608
+ try:
1609
+ await session._send(self._font_default_msg())
1610
+ except Exception:
1611
+ pass
1612
+
1613
+ def _get_font_bytes(self, font_id):
1614
+ with self._font_lock:
1615
+ entry = self._fonts_by_id.get(font_id)
1616
+ if entry is None:
1617
+ return None, None
1618
+ return entry["bytes"], entry["mime"]
1619
+
1496
1620
  @property
1497
1621
  def sessions(self):
1498
1622
  """Dict of active sessions (session_id -> Session)."""
@@ -1603,6 +1727,10 @@ class Application:
1603
1727
 
1604
1728
  if is_reconnect:
1605
1729
  async def do_reconstruct():
1730
+ # Replay the font registry before reconstruct() so
1731
+ # any widget reconstructed with ``set_font(...)``
1732
+ # finds the face already declared.
1733
+ await self._replay_fonts_to_session(session)
1606
1734
  self._logger.info(
1607
1735
  f"Session {session.id}: reconstructing UI.")
1608
1736
  session._reconstructing = True
@@ -1615,10 +1743,16 @@ class Application:
1615
1743
  asyncio.ensure_future(do_reconstruct())
1616
1744
  else:
1617
1745
  self._logger.info(f"Session {session.id} connected.")
1618
- if self._on_connect:
1619
- result = self._on_connect(session)
1620
- if hasattr(result, "__await__"):
1621
- asyncio.ensure_future(result)
1746
+ async def do_connect():
1747
+ # Replay fonts before the user callback so any
1748
+ # widget the user builds with ``set_font(family,
1749
+ # ...)`` sees the face already declared.
1750
+ await self._replay_fonts_to_session(session)
1751
+ if self._on_connect:
1752
+ result = self._on_connect(session)
1753
+ if hasattr(result, "__await__"):
1754
+ await result
1755
+ asyncio.ensure_future(do_connect())
1622
1756
 
1623
1757
  try:
1624
1758
  async for message in ws:
@@ -1663,8 +1797,14 @@ class Application:
1663
1797
  The newly created session.
1664
1798
  """
1665
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
1666
1803
  session_id = self._next_session_id
1667
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
1668
1808
  elif session_id in self._sessions:
1669
1809
  # If the existing session was auto-created by a browser
1670
1810
  # reconnect with the same token, return it so the user's
@@ -1699,6 +1839,7 @@ class Application:
1699
1839
  favicon_path = self._favicon_path
1700
1840
  ws_host = self._host
1701
1841
  ws_port = self._ws_port
1842
+ app = self
1702
1843
 
1703
1844
  class Handler(SimpleHTTPRequestHandler):
1704
1845
  def __init__(self, *a, **kw):
@@ -1707,6 +1848,27 @@ class Application:
1707
1848
  def do_GET(self):
1708
1849
  # Strip query string for path matching (e.g. /?session=1)
1709
1850
  path = self.path.split("?")[0]
1851
+ # Custom-font registry: see sync.application for
1852
+ # the matching implementation.
1853
+ if path.startswith("/_pgwidgets/font/"):
1854
+ try:
1855
+ font_id = int(path.rsplit("/", 1)[-1])
1856
+ except ValueError:
1857
+ self.send_error(404)
1858
+ return
1859
+ data, mime = app._get_font_bytes(font_id)
1860
+ if data is None:
1861
+ self.send_error(404)
1862
+ return
1863
+ self.send_response(200)
1864
+ self.send_header("Content-Type", mime)
1865
+ self.send_header("Content-Length", str(len(data)))
1866
+ self.send_header(
1867
+ "Cache-Control", "public, max-age=31536000, immutable")
1868
+ self.send_header("Access-Control-Allow-Origin", "*")
1869
+ self.end_headers()
1870
+ self.wfile.write(data)
1871
+ return
1710
1872
  if path == "/" or path == "/index.html":
1711
1873
  html = remote_html.read_text(encoding="utf-8")
1712
1874
  inject = (
@@ -1807,12 +1969,14 @@ class Application:
1807
1969
 
1808
1970
  In the async API, callbacks are dispatched by the asyncio event
1809
1971
  loop directly, so this simply yields control for the requested
1810
- duration.
1972
+ duration. A *timeout* of ``0`` yields once (a non-blocking drain
1973
+ of ready callbacks), matching the sync API's contract.
1811
1974
 
1812
1975
  Parameters
1813
1976
  ----------
1814
1977
  timeout : float
1815
1978
  Seconds to yield to the event loop (default 0.1).
1979
+ Use ``0`` for a single non-blocking yield.
1816
1980
  """
1817
1981
  await asyncio.sleep(timeout)
1818
1982
 
@@ -446,6 +446,11 @@ def _resolve_kwargs(method_name, param_names, args, kwargs):
446
446
  are bundled into a dict for that parameter (e.g.
447
447
  ``add_widget(child, title="Tab 1")`` becomes
448
448
  ``add_widget(child, {"title": "Tab 1"})``).
449
+
450
+ Skipped-positional kwargs are supported: a call like
451
+ ``set_color(fg='red')`` against ``param_names = ['bg', 'fg']``
452
+ fills the omitted ``bg`` slot with ``None`` (the JS-side
453
+ default) instead of erroring out.
449
454
  """
450
455
  if not kwargs:
451
456
  return args
@@ -456,7 +461,10 @@ def _resolve_kwargs(method_name, param_names, args, kwargs):
456
461
  if name in kwargs:
457
462
  merged.append(kwargs.pop(name))
458
463
  else:
459
- break
464
+ # Leave a placeholder so subsequent kwargs can land in
465
+ # later positions. The JS side reads omitted args as
466
+ # null / default, which matches ``None`` here.
467
+ merged.append(None)
460
468
  if kwargs and param_names and param_names[-1] == "options":
461
469
  # Bundle remaining kwargs into the options dict
462
470
  opts_idx = len(param_names) - 1
@@ -352,8 +352,13 @@ class FileBrowser(Callbacks):
352
352
  self._navigate_to(d)
353
353
  self._name_entry.set_text(os.path.basename(path))
354
354
 
355
- def _on_row_activated(self, values, path):
356
- """Double-click on a row."""
355
+ def _on_row_activated(self, values, path, col_key=None):
356
+ """Double-click on a row.
357
+
358
+ ``col_key`` (TableView ≥ this rev) reports which cell was
359
+ clicked; unused here since the browser only cares about
360
+ the row's filename, but the arg has to be in the signature
361
+ or the new 3-arg dispatch raises TypeError."""
357
362
  name = values.get("name", "")
358
363
  if name == "..":
359
364
  self._go_up()
pgwidgets/method_types.py CHANGED
@@ -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
@@ -82,6 +82,17 @@ ACTION_METHODS = {
82
82
  "expand_all", "collapse_all", "expand_item", "collapse_item",
83
83
  "sort_by_column", "set_optimal_column_widths",
84
84
  "select_path", "select_paths", "select_all",
85
+ "select_cell", "select_cells", "clear_cell_selection",
86
+ # Per-cell / row / column / table colour overrides — the
87
+ # ``set_*`` naming would otherwise classify them as SETTERs,
88
+ # whose single-state-slot semantics can't represent the
89
+ # accumulated dict of overrides we actually keep. ACTION
90
+ # dispatch sends each call straight through to the JS side,
91
+ # which holds the canonical state in its own maps.
92
+ "set_cell_color", "set_row_color", "set_column_color",
93
+ "set_table_color",
94
+ "clear_cell_color", "clear_row_color", "clear_column_color",
95
+ "clear_all_colors",
85
96
  # TextSource editing
86
97
  "insert_text", "delete_range", "create_tag", "remove_tag_def",
87
98
  "apply_tag", "remove_tag", "create_ref", "remove_ref",
@@ -130,6 +130,17 @@ def _run_queue_loop(cb_queue, stop_event, logger=None):
130
130
  result_slot['event'].set()
131
131
 
132
132
 
133
+ # MIME types for the font formats register_font accepts. Used to
134
+ # set the ``Content-Type`` header when the HTTP server delivers a
135
+ # registered font to the browser.
136
+ _FONT_MIME = {
137
+ ".ttf": "font/ttf",
138
+ ".otf": "font/otf",
139
+ ".woff": "font/woff",
140
+ ".woff2": "font/woff2",
141
+ }
142
+
143
+
133
144
  class Session:
134
145
  """
135
146
  A session that owns a widget tree and its associated state.
@@ -1642,6 +1653,17 @@ class Application:
1642
1653
  self._on_disconnect = None # user callback: fn(session)
1643
1654
  self._cb_queue = queue.Queue() # for "serialized" mode
1644
1655
 
1656
+ # Custom-font registry. Each entry is
1657
+ # ``{id, family, weight, style, bytes, mime}``; ``id`` is a
1658
+ # monotonic int used as the path component of the HTTP URL
1659
+ # the browser fetches the font from. ``_default_font`` is
1660
+ # ``{family, size, weight, style}`` or ``None``.
1661
+ self._fonts = []
1662
+ self._fonts_by_id = {}
1663
+ self._next_font_id = 1
1664
+ self._default_font = None
1665
+ self._font_lock = threading.Lock()
1666
+
1645
1667
  self._loop = None
1646
1668
  self._shutdown = threading.Event()
1647
1669
  self._thread = None
@@ -1714,6 +1736,155 @@ class Application:
1714
1736
  self._widget_classes[name] = cls
1715
1737
  return cls
1716
1738
 
1739
+ # ----- Custom font registration ---------------------------
1740
+
1741
+ def register_font(self, family, source, *,
1742
+ weight="normal", style="normal"):
1743
+ """Register a custom font with the application.
1744
+
1745
+ Browsers receive a ``@font-face``-equivalent declaration
1746
+ through the JS ``FontFace`` API, so any widget that does
1747
+ ``set_font(family, ...)`` thereafter renders with this
1748
+ face. Multiple registrations for the same ``family`` but
1749
+ different ``weight`` / ``style`` combine into a single
1750
+ font family with multiple faces.
1751
+
1752
+ Parameters
1753
+ ----------
1754
+ family : str
1755
+ CSS font-family name to expose to widgets.
1756
+ source : str | os.PathLike | bytes | bytearray | memoryview
1757
+ Path to a font file (``.ttf`` / ``.otf`` / ``.woff`` /
1758
+ ``.woff2``) or raw font bytes.
1759
+ weight : str
1760
+ CSS ``font-weight`` -- ``'normal'``, ``'bold'``, or a
1761
+ numeric string (``'100'`` ... ``'900'``).
1762
+ style : str
1763
+ CSS ``font-style`` -- ``'normal'``, ``'italic'``, or
1764
+ ``'oblique'``.
1765
+
1766
+ Returns
1767
+ -------
1768
+ int
1769
+ The registration id (rarely needed; callers usually
1770
+ just refer to the font by ``family``).
1771
+ """
1772
+ if isinstance(source, (bytes, bytearray, memoryview)):
1773
+ data = bytes(source)
1774
+ mime = "font/ttf"
1775
+ else:
1776
+ p = Path(source)
1777
+ data = p.read_bytes()
1778
+ ext = p.suffix.lower()
1779
+ mime = _FONT_MIME.get(ext, "font/ttf")
1780
+ with self._font_lock:
1781
+ font_id = self._next_font_id
1782
+ self._next_font_id += 1
1783
+ entry = {
1784
+ "id": font_id,
1785
+ "family": str(family),
1786
+ "weight": str(weight),
1787
+ "style": str(style),
1788
+ "bytes": data,
1789
+ "mime": mime,
1790
+ }
1791
+ self._fonts.append(entry)
1792
+ self._fonts_by_id[font_id] = entry
1793
+ # Push to any already-connected sessions so live UIs pick
1794
+ # the font up without a reconnect.
1795
+ msg = self._font_register_msg(entry)
1796
+ for session in list(self._sessions.values()):
1797
+ try:
1798
+ session._send(msg)
1799
+ except Exception:
1800
+ pass
1801
+ return font_id
1802
+
1803
+ def set_default_font(self, family, *, size=None,
1804
+ weight=None, style=None):
1805
+ """Set the document-level default font.
1806
+
1807
+ Writes ``--pg-default-font-family`` / ``--pg-default-font-size``
1808
+ / ``--pg-default-font-weight`` / ``--pg-default-font-style``
1809
+ CSS variables on ``:root``; the base pgwidgets stylesheet
1810
+ consumes these so any widget that hasn't been given an
1811
+ explicit ``set_font(...)`` follows the default.
1812
+
1813
+ Pass ``family=None`` to clear the default and fall back to
1814
+ the built-in stylesheet."""
1815
+ if family is None:
1816
+ self._default_font = None
1817
+ else:
1818
+ self._default_font = {
1819
+ "family": str(family),
1820
+ "size": None if size is None else float(size),
1821
+ "weight": None if weight is None else str(weight),
1822
+ "style": None if style is None else str(style),
1823
+ }
1824
+ msg = self._font_default_msg()
1825
+ for session in list(self._sessions.values()):
1826
+ try:
1827
+ session._send(msg)
1828
+ except Exception:
1829
+ pass
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
+
1845
+ def _font_register_msg(self, entry):
1846
+ return {
1847
+ "type": "register-font",
1848
+ "id": entry["id"],
1849
+ "family": entry["family"],
1850
+ "weight": entry["weight"],
1851
+ "style": entry["style"],
1852
+ "url": f"/_pgwidgets/font/{entry['id']}",
1853
+ }
1854
+
1855
+ def _font_default_msg(self):
1856
+ return {
1857
+ "type": "set-default-font",
1858
+ "font": self._default_font,
1859
+ }
1860
+
1861
+ def _replay_fonts_to_session(self, session):
1862
+ """Push the full font registry + default font to a freshly
1863
+ connected (or reconnecting) session before any user code
1864
+ creates widgets. Called from ``_on_session_open``."""
1865
+ with self._font_lock:
1866
+ fonts = list(self._fonts)
1867
+ default = self._default_font
1868
+ for entry in fonts:
1869
+ try:
1870
+ session._send(self._font_register_msg(entry))
1871
+ except Exception:
1872
+ pass
1873
+ if default is not None:
1874
+ try:
1875
+ session._send(self._font_default_msg())
1876
+ except Exception:
1877
+ pass
1878
+
1879
+ def _get_font_bytes(self, font_id):
1880
+ """Return ``(bytes, mime)`` for a registered font, or
1881
+ ``(None, None)`` if unknown. Called by the HTTP handler."""
1882
+ with self._font_lock:
1883
+ entry = self._fonts_by_id.get(font_id)
1884
+ if entry is None:
1885
+ return None, None
1886
+ return entry["bytes"], entry["mime"]
1887
+
1717
1888
  def start(self):
1718
1889
  """Start the WebSocket server (and HTTP server if enabled).
1719
1890
 
@@ -1836,6 +2007,10 @@ class Application:
1836
2007
  # because _send() blocks with event.wait() while the event
1837
2008
  # loop that must process ws.send() is also blocked.
1838
2009
  def do_reconstruct():
2010
+ # Replay the font registry before reconstruct() so any
2011
+ # widget reconstructed with ``set_font(...)`` finds the
2012
+ # face already declared.
2013
+ self._replay_fonts_to_session(session)
1839
2014
  self._logger.info(
1840
2015
  f"Session {session.id}: reconstructing UI.")
1841
2016
  session._reconstructing = True
@@ -1848,8 +2023,14 @@ class Application:
1848
2023
  self._dispatch(session, do_reconstruct, ())
1849
2024
  else:
1850
2025
  self._logger.info(f"Session {session.id} connected.")
1851
- if self._on_connect:
1852
- self._dispatch(session, self._on_connect, (session,))
2026
+ def do_connect():
2027
+ # Replay fonts before the user callback so any widget
2028
+ # the user builds with ``set_font(family, ...)`` sees
2029
+ # the face already declared.
2030
+ self._replay_fonts_to_session(session)
2031
+ if self._on_connect:
2032
+ self._on_connect(session)
2033
+ self._dispatch(session, do_connect, ())
1853
2034
 
1854
2035
  try:
1855
2036
  async for message in ws:
@@ -1895,8 +2076,14 @@ class Application:
1895
2076
  """
1896
2077
  with self._session_lock:
1897
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
1898
2082
  session_id = self._next_session_id
1899
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
1900
2087
  elif session_id in self._sessions:
1901
2088
  # If the existing session was auto-created by a browser
1902
2089
  # reconnect with the same token, return it so the user's
@@ -1942,6 +2129,7 @@ class Application:
1942
2129
  favicon_path = self._favicon_path
1943
2130
  ws_host = self._host
1944
2131
  ws_port = self._ws_port
2132
+ app = self
1945
2133
 
1946
2134
  class Handler(http.server.SimpleHTTPRequestHandler):
1947
2135
  def __init__(self, *args, **kwargs):
@@ -1951,6 +2139,31 @@ class Application:
1951
2139
  # serve remote.html at the root, with WS URL injected
1952
2140
  # Strip query string for path matching (e.g. /?session=1)
1953
2141
  path = self.path.split("?")[0]
2142
+ # Custom-font registry: /_pgwidgets/font/<id> serves
2143
+ # the bytes the app registered via ``register_font``.
2144
+ # ``Cache-Control: immutable`` is safe because the
2145
+ # registry assigns a fresh id on every call -- the
2146
+ # URL is content-stable for the lifetime of the
2147
+ # registration.
2148
+ if path.startswith("/_pgwidgets/font/"):
2149
+ try:
2150
+ font_id = int(path.rsplit("/", 1)[-1])
2151
+ except ValueError:
2152
+ self.send_error(404)
2153
+ return
2154
+ data, mime = app._get_font_bytes(font_id)
2155
+ if data is None:
2156
+ self.send_error(404)
2157
+ return
2158
+ self.send_response(200)
2159
+ self.send_header("Content-Type", mime)
2160
+ self.send_header("Content-Length", str(len(data)))
2161
+ self.send_header(
2162
+ "Cache-Control", "public, max-age=31536000, immutable")
2163
+ self.send_header("Access-Control-Allow-Origin", "*")
2164
+ self.end_headers()
2165
+ self.wfile.write(data)
2166
+ return
1954
2167
  if path == "/" or path == "/index.html":
1955
2168
  html = remote_html.read_text(encoding="utf-8")
1956
2169
  inject = (
@@ -2086,22 +2299,35 @@ class Application:
2086
2299
  ``concurrent`` modes, callbacks already run on their own threads
2087
2300
  so this method simply yields control briefly.
2088
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
+
2089
2306
  Parameters
2090
2307
  ----------
2091
2308
  timeout : float
2092
2309
  Maximum seconds to spend processing events (default 0.1).
2310
+ Use ``0`` for a non-blocking drain of the pending queue.
2093
2311
  """
2094
2312
  if self._concurrency == "serialized":
2313
+ non_blocking = (timeout == 0)
2095
2314
  deadline = time.monotonic() + timeout
2096
2315
  while not self._shutdown.is_set():
2097
- remaining = deadline - time.monotonic()
2098
- if remaining <= 0:
2099
- break
2100
- try:
2101
- handler, args, kwargs, result_slot = \
2102
- self._cb_queue.get(timeout=min(remaining, 0.5))
2103
- except queue.Empty:
2104
- 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
2105
2331
  try:
2106
2332
  rv = handler(*args, **kwargs)
2107
2333
  if result_slot is not None:
pgwidgets/sync/widget.py CHANGED
@@ -420,6 +420,11 @@ def _resolve_kwargs(method_name, param_names, args, kwargs):
420
420
  are bundled into a dict for that parameter (e.g.
421
421
  ``add_widget(child, title="Tab 1")`` becomes
422
422
  ``add_widget(child, {"title": "Tab 1"})``).
423
+
424
+ Skipped-positional kwargs are supported: a call like
425
+ ``set_color(fg='red')`` against ``param_names = ['bg', 'fg']``
426
+ fills the omitted ``bg`` slot with ``None`` (the JS-side
427
+ default) instead of erroring out.
423
428
  """
424
429
  if not kwargs:
425
430
  return args
@@ -430,7 +435,10 @@ def _resolve_kwargs(method_name, param_names, args, kwargs):
430
435
  if name in kwargs:
431
436
  merged.append(kwargs.pop(name))
432
437
  else:
433
- break
438
+ # Leave a placeholder so subsequent kwargs can land in
439
+ # later positions. The JS side reads omitted args as
440
+ # null / default, which matches ``None`` here.
441
+ merged.append(None)
434
442
  if kwargs and param_names and param_names[-1] == "options":
435
443
  # Bundle remaining kwargs into the options dict
436
444
  opts_idx = len(param_names) - 1
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pgwidgets-python
3
- Version: 0.3.0
3
+ Version: 0.3.2
4
4
  Summary: Python bindings for the pgwidgets JavaScript widget library
5
5
  Author: PGWidgets Developers
6
6
  License: BSD-3-Clause
@@ -3,19 +3,19 @@ pgwidgets/_json.py,sha256=o21qywJ6yAldbqxTq3nLwgK9O67r3a8JxhvI_WAJnMY,2184
3
3
  pgwidgets/buffer.py,sha256=BYj_nb6fuyGsBqUp3_Z1YrzUm4h5VmoWPGKvpTjWrYk,4048
4
4
  pgwidgets/callbacks.py,sha256=gA2FnX0N5BmbmOMyEV35yHySgAfVjfAZcZhZbo7j4-c,3314
5
5
  pgwidgets/defs.py,sha256=Q8qhvTeansFU1Av6j5ncdwF2xSNHrpXuKErdHWQ3HiA,436
6
- pgwidgets/method_types.py,sha256=E_f0CQtDwWKgiPmIPGEftm3OXvntErAY4JZH0C7Dwuo,17699
6
+ pgwidgets/method_types.py,sha256=_-rdE5BOYLra8x3BMKv5-QS4Z8YB29DBhUCDsGiB-Gw,18314
7
7
  pgwidgets/async_/Widgets.py,sha256=vld2xkvusBBEVbhbezaJsjBRRj3L7c17Up0pOVs8PGo,723
8
8
  pgwidgets/async_/__init__.py,sha256=rXB-v9XRrYt1imYuYikhkzIyiRqaYhlTpQei2LHaQ18,564
9
- pgwidgets/async_/application.py,sha256=xLt7ZF7jgRYKT5-L95sahohT4Omq2TfW7y8V1f6GM9g,74443
10
- pgwidgets/async_/widget.py,sha256=H27v7AzHNg5obzr2e8UyOyj2lbGfWhEhZUrIDqrZNbg,37612
9
+ pgwidgets/async_/application.py,sha256=2RJNRerXu5LXG_UcMSi9HWOCAlEOu96TrRaoIcUZ86Q,80957
10
+ pgwidgets/async_/widget.py,sha256=gO7eJh9LV3UY7yRLdeml_0gqmHnFB5VU3NWlDURLtfc,38042
11
11
  pgwidgets/extras/__init__.py,sha256=AXUmFtnn4RSlp6pJX6z55j-m_29VmfzpYIjQvAy7pO0,325
12
- pgwidgets/extras/file_browser.py,sha256=PWzJltjQZbmsD1ykbbC2xJ6ErEkJj7KafhmVkd9q9Ac,16963
12
+ pgwidgets/extras/file_browser.py,sha256=CXQSfJx1VQ-fJfhKNrVyqrkKTJEQngmiP954y70Fjo0,17229
13
13
  pgwidgets/sync/Widgets.py,sha256=7SaocMVMzFHhi51pjuEAr47Wez90b_a61kDbiv0jOfM,706
14
14
  pgwidgets/sync/__init__.py,sha256=SF5RTAvtu8BbYBWzpiCPipy6DJzNXf6nqk9xdvwxUhQ,542
15
- pgwidgets/sync/application.py,sha256=0xZDEywx7L7vUe1QAs29iufIqqvmSzOLeu9QQIGiSj0,87160
16
- pgwidgets/sync/widget.py,sha256=tQDkCOkcevDrGnG4LhBf69law82-UB1fF-r08GUZPSE,37913
17
- pgwidgets_python-0.3.0.dist-info/licenses/LICENSE.md,sha256=LoM3fMTiMnQuHRCJghdjOtjnCrL8soBpu2PFk24Xvyg,1528
18
- pgwidgets_python-0.3.0.dist-info/METADATA,sha256=LtcjQx8O8cbpQ_1HcXVm8Jpo7UyFLfVkPCEe8fEmJIY,4568
19
- pgwidgets_python-0.3.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
20
- pgwidgets_python-0.3.0.dist-info/top_level.txt,sha256=wwL6fBq0gU-JwzlM6TdduY1qYpu39ysqnnbQT-1bqAs,10
21
- pgwidgets_python-0.3.0.dist-info/RECORD,,
15
+ pgwidgets/sync/application.py,sha256=tcAJvSVdijfT_KBP4llY1tyg2AAmQM-1qocw8Zf51nM,96240
16
+ pgwidgets/sync/widget.py,sha256=yrScNkZB34u0JXn3-kCB-1mPLxLysVbuaPsCjC8kq0E,38343
17
+ pgwidgets_python-0.3.2.dist-info/licenses/LICENSE.md,sha256=LoM3fMTiMnQuHRCJghdjOtjnCrL8soBpu2PFk24Xvyg,1528
18
+ pgwidgets_python-0.3.2.dist-info/METADATA,sha256=gzz5UD--Uq8I9azawdmXRE-G4KNDrTFDtkVhiNbTSzs,4568
19
+ pgwidgets_python-0.3.2.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
20
+ pgwidgets_python-0.3.2.dist-info/top_level.txt,sha256=wwL6fBq0gU-JwzlM6TdduY1qYpu39ysqnnbQT-1bqAs,10
21
+ pgwidgets_python-0.3.2.dist-info/RECORD,,