flet-charts 1.0.0.dev0__tar.gz → 1.0.0.dev2__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.

Potentially problematic release.


This version of flet-charts might be problematic. Click here for more details.

Files changed (53) hide show
  1. {flet_charts-1.0.0.dev0/src/flet_charts.egg-info → flet_charts-1.0.0.dev2}/PKG-INFO +2 -2
  2. {flet_charts-1.0.0.dev0 → flet_charts-1.0.0.dev2}/pyproject.toml +2 -2
  3. {flet_charts-1.0.0.dev0 → flet_charts-1.0.0.dev2}/src/flet_charts/matplotlib_chart.py +91 -67
  4. {flet_charts-1.0.0.dev0 → flet_charts-1.0.0.dev2/src/flet_charts.egg-info}/PKG-INFO +2 -2
  5. {flet_charts-1.0.0.dev0 → flet_charts-1.0.0.dev2}/src/flet_charts.egg-info/SOURCES.txt +2 -1
  6. flet_charts-1.0.0.dev2/src/flet_charts.egg-info/requires.txt +1 -0
  7. {flet_charts-1.0.0.dev0 → flet_charts-1.0.0.dev2}/tests/test_matplotlib_chart_canvas.py +1 -0
  8. flet_charts-1.0.0.dev2/tests/test_matplotlib_chart_receive_loop.py +122 -0
  9. flet_charts-1.0.0.dev0/src/flet_charts.egg-info/requires.txt +0 -1
  10. {flet_charts-1.0.0.dev0 → flet_charts-1.0.0.dev2}/LICENSE +0 -0
  11. {flet_charts-1.0.0.dev0 → flet_charts-1.0.0.dev2}/README.md +0 -0
  12. {flet_charts-1.0.0.dev0 → flet_charts-1.0.0.dev2}/setup.cfg +0 -0
  13. {flet_charts-1.0.0.dev0 → flet_charts-1.0.0.dev2}/src/flet_charts/__init__.py +0 -0
  14. {flet_charts-1.0.0.dev0 → flet_charts-1.0.0.dev2}/src/flet_charts/bar_chart.py +0 -0
  15. {flet_charts-1.0.0.dev0 → flet_charts-1.0.0.dev2}/src/flet_charts/bar_chart_group.py +0 -0
  16. {flet_charts-1.0.0.dev0 → flet_charts-1.0.0.dev2}/src/flet_charts/bar_chart_rod.py +0 -0
  17. {flet_charts-1.0.0.dev0 → flet_charts-1.0.0.dev2}/src/flet_charts/bar_chart_rod_stack_item.py +0 -0
  18. {flet_charts-1.0.0.dev0 → flet_charts-1.0.0.dev2}/src/flet_charts/candlestick_chart.py +0 -0
  19. {flet_charts-1.0.0.dev0 → flet_charts-1.0.0.dev2}/src/flet_charts/candlestick_chart_spot.py +0 -0
  20. {flet_charts-1.0.0.dev0 → flet_charts-1.0.0.dev2}/src/flet_charts/chart_axis.py +0 -0
  21. {flet_charts-1.0.0.dev0 → flet_charts-1.0.0.dev2}/src/flet_charts/line_chart.py +0 -0
  22. {flet_charts-1.0.0.dev0 → flet_charts-1.0.0.dev2}/src/flet_charts/line_chart_data.py +0 -0
  23. {flet_charts-1.0.0.dev0 → flet_charts-1.0.0.dev2}/src/flet_charts/line_chart_data_point.py +0 -0
  24. {flet_charts-1.0.0.dev0 → flet_charts-1.0.0.dev2}/src/flet_charts/matplotlib_backends/backend_flet_agg.py +0 -0
  25. {flet_charts-1.0.0.dev0 → flet_charts-1.0.0.dev2}/src/flet_charts/matplotlib_chart_canvas.py +0 -0
  26. {flet_charts-1.0.0.dev0 → flet_charts-1.0.0.dev2}/src/flet_charts/matplotlib_chart_with_toolbar.py +0 -0
  27. {flet_charts-1.0.0.dev0 → flet_charts-1.0.0.dev2}/src/flet_charts/pie_chart.py +0 -0
  28. {flet_charts-1.0.0.dev0 → flet_charts-1.0.0.dev2}/src/flet_charts/pie_chart_section.py +0 -0
  29. {flet_charts-1.0.0.dev0 → flet_charts-1.0.0.dev2}/src/flet_charts/plotly_chart.py +0 -0
  30. {flet_charts-1.0.0.dev0 → flet_charts-1.0.0.dev2}/src/flet_charts/radar_chart.py +0 -0
  31. {flet_charts-1.0.0.dev0 → flet_charts-1.0.0.dev2}/src/flet_charts/radar_data_set.py +0 -0
  32. {flet_charts-1.0.0.dev0 → flet_charts-1.0.0.dev2}/src/flet_charts/scatter_chart.py +0 -0
  33. {flet_charts-1.0.0.dev0 → flet_charts-1.0.0.dev2}/src/flet_charts/scatter_chart_spot.py +0 -0
  34. {flet_charts-1.0.0.dev0 → flet_charts-1.0.0.dev2}/src/flet_charts/types.py +0 -0
  35. {flet_charts-1.0.0.dev0 → flet_charts-1.0.0.dev2}/src/flet_charts.egg-info/dependency_links.txt +0 -0
  36. {flet_charts-1.0.0.dev0 → flet_charts-1.0.0.dev2}/src/flet_charts.egg-info/top_level.txt +0 -0
  37. {flet_charts-1.0.0.dev0 → flet_charts-1.0.0.dev2}/src/flutter/flet_charts/lib/flet_charts.dart +0 -0
  38. {flet_charts-1.0.0.dev0 → flet_charts-1.0.0.dev2}/src/flutter/flet_charts/lib/src/bar_chart.dart +0 -0
  39. {flet_charts-1.0.0.dev0 → flet_charts-1.0.0.dev2}/src/flutter/flet_charts/lib/src/candlestick_chart.dart +0 -0
  40. {flet_charts-1.0.0.dev0 → flet_charts-1.0.0.dev2}/src/flutter/flet_charts/lib/src/extension.dart +0 -0
  41. {flet_charts-1.0.0.dev0 → flet_charts-1.0.0.dev2}/src/flutter/flet_charts/lib/src/line_chart.dart +0 -0
  42. {flet_charts-1.0.0.dev0 → flet_charts-1.0.0.dev2}/src/flutter/flet_charts/lib/src/matplotlib_chart_canvas.dart +0 -0
  43. {flet_charts-1.0.0.dev0 → flet_charts-1.0.0.dev2}/src/flutter/flet_charts/lib/src/pie_chart.dart +0 -0
  44. {flet_charts-1.0.0.dev0 → flet_charts-1.0.0.dev2}/src/flutter/flet_charts/lib/src/radar_chart.dart +0 -0
  45. {flet_charts-1.0.0.dev0 → flet_charts-1.0.0.dev2}/src/flutter/flet_charts/lib/src/scatter_chart.dart +0 -0
  46. {flet_charts-1.0.0.dev0 → flet_charts-1.0.0.dev2}/src/flutter/flet_charts/lib/src/utils/bar_chart.dart +0 -0
  47. {flet_charts-1.0.0.dev0 → flet_charts-1.0.0.dev2}/src/flutter/flet_charts/lib/src/utils/candlestick_chart.dart +0 -0
  48. {flet_charts-1.0.0.dev0 → flet_charts-1.0.0.dev2}/src/flutter/flet_charts/lib/src/utils/charts.dart +0 -0
  49. {flet_charts-1.0.0.dev0 → flet_charts-1.0.0.dev2}/src/flutter/flet_charts/lib/src/utils/line_chart.dart +0 -0
  50. {flet_charts-1.0.0.dev0 → flet_charts-1.0.0.dev2}/src/flutter/flet_charts/lib/src/utils/pie_chart.dart +0 -0
  51. {flet_charts-1.0.0.dev0 → flet_charts-1.0.0.dev2}/src/flutter/flet_charts/lib/src/utils/radar_chart.dart +0 -0
  52. {flet_charts-1.0.0.dev0 → flet_charts-1.0.0.dev2}/src/flutter/flet_charts/lib/src/utils/scatter_chart.dart +0 -0
  53. {flet_charts-1.0.0.dev0 → flet_charts-1.0.0.dev2}/src/flutter/flet_charts/pubspec.yaml +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flet-charts
3
- Version: 1.0.0.dev0
3
+ Version: 1.0.0.dev2
4
4
  Summary: Interactive chart controls for Flet apps.
5
5
  Author-email: Flet contributors <hello@flet.dev>
6
6
  License-Expression: Apache-2.0
@@ -11,7 +11,7 @@ Project-URL: Issues, https://github.com/flet-dev/flet/issues
11
11
  Requires-Python: >=3.10
12
12
  Description-Content-Type: text/markdown
13
13
  License-File: LICENSE
14
- Requires-Dist: flet==1.0.0.dev0
14
+ Requires-Dist: flet==1.0.0.dev2
15
15
  Dynamic: license-file
16
16
 
17
17
  # flet-charts
@@ -1,13 +1,13 @@
1
1
  [project]
2
2
  name = "flet-charts"
3
- version = "1.0.0.dev0"
3
+ version = "1.0.0.dev2"
4
4
  description = "Interactive chart controls for Flet apps."
5
5
  readme = "README.md"
6
6
  authors = [{ name = "Flet contributors", email = "hello@flet.dev" }]
7
7
  license = "Apache-2.0"
8
8
  requires-python = ">=3.10"
9
9
  dependencies = [
10
- "flet==1.0.0.dev0",
10
+ "flet==1.0.0.dev2",
11
11
  ]
12
12
 
13
13
  [project.urls]
@@ -433,79 +433,103 @@ class MatplotlibChart(ft.GestureDetector):
433
433
  """
434
434
  Consume backend messages and apply canvas/state updates.
435
435
 
436
- The loop handles both binary image frames and JSON control messages
437
- (cursor updates, draw requests, rubber-band overlays, status text, and
438
- toolbar history state).
436
+ Message handling is guarded: applying a frame, and Matplotlib's own
437
+ rendering (which `send_message` runs synchronously, in this task),
438
+ can both raise. Letting that escape would end the loop - nothing
439
+ would drain `_receive_queue` again, so the chart would freeze
440
+ silently and the traceback would only surface later, as an
441
+ unretrieved task exception when the task is garbage-collected.
439
442
  """
440
443
 
441
444
  while True:
442
445
  is_binary, content = await self._receive_queue.get()
443
-
444
- if is_binary:
445
- # Hand the frame to the client widget — a raw RGBA frame
446
- # (pre-encoded 0x04 packet) or full PNG replaces the
447
- # backbuffer, diff PNG composites onto it. `await`
448
- # here serialises this receive loop on the Dart-side
449
- # frame-applied ack: matplotlib "draw" notifications that
450
- # arrive during the round-trip stay queued in
451
- # `_receive_queue` and are processed after the ack returns,
452
- # instead of being eagerly dropped against a stale
453
- # `_waiting=True` gate. This is the same backpressure shape
454
- # the 0.85 `_invoke_method` round-trip used to provide.
455
- if isinstance(content, tuple) and content[0] == "raw":
456
- logger.debug(f"receive_binary(raw, {len(content[1])})")
457
- await self.mpl_canvas.apply_raw_packet(content[1])
458
- elif self.__image_mode == "full":
459
- logger.debug(f"receive_binary(full, {len(content)})")
460
- await self.mpl_canvas.apply_full(bytes(content))
461
- else:
462
- logger.debug(f"receive_binary(diff, {len(content)})")
463
- await self.mpl_canvas.apply_diff(bytes(content))
464
- self.img_count += 1
446
+ try:
447
+ await self._handle_message(is_binary, content)
448
+ except asyncio.CancelledError:
449
+ raise
450
+ except Exception:
451
+ logger.exception("Error handling Matplotlib backend message")
452
+ # A draw that raised never produces the frame that clears
453
+ # this gate, so clear it here or no further draw is ever
454
+ # requested.
465
455
  self._waiting = False
456
+
457
+ async def _handle_message(self, is_binary: bool, content: Any) -> None:
458
+ """
459
+ Apply a single message from the Matplotlib backend.
460
+
461
+ Handles both binary image frames and JSON control messages (cursor
462
+ updates, draw requests, rubber-band overlays, status text, and
463
+ toolbar history state).
464
+
465
+ Args:
466
+ is_binary: Whether `content` is an image frame rather than JSON.
467
+ content: Frame payload or JSON control message.
468
+ """
469
+
470
+ if is_binary:
471
+ # Hand the frame to the client widget — a raw RGBA frame
472
+ # (pre-encoded 0x04 packet) or full PNG replaces the
473
+ # backbuffer, diff PNG composites onto it. `await`
474
+ # here serialises this receive loop on the Dart-side
475
+ # frame-applied ack: matplotlib "draw" notifications that
476
+ # arrive during the round-trip stay queued in
477
+ # `_receive_queue` and are processed after the ack returns,
478
+ # instead of being eagerly dropped against a stale
479
+ # `_waiting=True` gate. This is the same backpressure shape
480
+ # the 0.85 `_invoke_method` round-trip used to provide.
481
+ if isinstance(content, tuple) and content[0] == "raw":
482
+ logger.debug(f"receive_binary(raw, {len(content[1])})")
483
+ await self.mpl_canvas.apply_raw_packet(content[1])
484
+ elif self.__image_mode == "full":
485
+ logger.debug(f"receive_binary(full, {len(content)})")
486
+ await self.mpl_canvas.apply_full(bytes(content))
466
487
  else:
467
- logger.debug(f"receive_json({content})")
468
- if content["type"] == "image_mode":
469
- self.__image_mode = content["mode"]
470
- elif content["type"] == "cursor":
471
- self.mouse_cursor = figure_cursors[content["cursor"]]
472
- self.update()
473
- elif content["type"] == "draw" and not self._waiting:
474
- self._waiting = True
475
- self.send_message({"type": "draw"})
476
- elif content["type"] == "rubberband":
477
- if (
478
- content["x0"] != -1
479
- and content["y0"] != -1
480
- and content["x1"] != -1
481
- and content["y1"] != -1
482
- ):
483
- x0 = content["x0"] / self.__dpr
484
- y0 = self._height - content["y0"] / self.__dpr
485
- x1 = content["x1"] / self.__dpr
486
- y1 = self._height - content["y1"] / self.__dpr
487
- self._rubberband.left = min(x0, x1)
488
- self._rubberband.top = min(y0, y1)
489
- self._rubberband.width = abs(x1 - x0)
490
- self._rubberband.height = abs(y1 - y0)
491
- self._rubberband.visible = True
492
- else:
493
- self._rubberband.visible = False
494
- self._rubberband.update()
495
- elif content["type"] == "resize":
496
- self.send_message({"type": "refresh"})
497
- elif content["type"] == "message":
498
- await self._trigger_event(
499
- "message", {"message": content["message"]}
500
- )
501
- elif content["type"] == "history_buttons":
502
- await self._trigger_event(
503
- "toolbar_buttons_update",
504
- {
505
- "back_enabled": content["Back"],
506
- "forward_enabled": content["Forward"],
507
- },
508
- )
488
+ logger.debug(f"receive_binary(diff, {len(content)})")
489
+ await self.mpl_canvas.apply_diff(bytes(content))
490
+ self.img_count += 1
491
+ self._waiting = False
492
+ else:
493
+ logger.debug(f"receive_json({content})")
494
+ if content["type"] == "image_mode":
495
+ self.__image_mode = content["mode"]
496
+ elif content["type"] == "cursor":
497
+ self.mouse_cursor = figure_cursors[content["cursor"]]
498
+ self.update()
499
+ elif content["type"] == "draw" and not self._waiting:
500
+ self._waiting = True
501
+ self.send_message({"type": "draw"})
502
+ elif content["type"] == "rubberband":
503
+ if (
504
+ content["x0"] != -1
505
+ and content["y0"] != -1
506
+ and content["x1"] != -1
507
+ and content["y1"] != -1
508
+ ):
509
+ x0 = content["x0"] / self.__dpr
510
+ y0 = self._height - content["y0"] / self.__dpr
511
+ x1 = content["x1"] / self.__dpr
512
+ y1 = self._height - content["y1"] / self.__dpr
513
+ self._rubberband.left = min(x0, x1)
514
+ self._rubberband.top = min(y0, y1)
515
+ self._rubberband.width = abs(x1 - x0)
516
+ self._rubberband.height = abs(y1 - y0)
517
+ self._rubberband.visible = True
518
+ else:
519
+ self._rubberband.visible = False
520
+ self._rubberband.update()
521
+ elif content["type"] == "resize":
522
+ self.send_message({"type": "refresh"})
523
+ elif content["type"] == "message":
524
+ await self._trigger_event("message", {"message": content["message"]})
525
+ elif content["type"] == "history_buttons":
526
+ await self._trigger_event(
527
+ "toolbar_buttons_update",
528
+ {
529
+ "back_enabled": content["Back"],
530
+ "forward_enabled": content["Forward"],
531
+ },
532
+ )
509
533
 
510
534
  def send_message(self, message):
511
535
  """Sends a message to the figure's canvas manager."""
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flet-charts
3
- Version: 1.0.0.dev0
3
+ Version: 1.0.0.dev2
4
4
  Summary: Interactive chart controls for Flet apps.
5
5
  Author-email: Flet contributors <hello@flet.dev>
6
6
  License-Expression: Apache-2.0
@@ -11,7 +11,7 @@ Project-URL: Issues, https://github.com/flet-dev/flet/issues
11
11
  Requires-Python: >=3.10
12
12
  Description-Content-Type: text/markdown
13
13
  License-File: LICENSE
14
- Requires-Dist: flet==1.0.0.dev0
14
+ Requires-Dist: flet==1.0.0.dev2
15
15
  Dynamic: license-file
16
16
 
17
17
  # flet-charts
@@ -46,4 +46,5 @@ src/flutter/flet_charts/lib/src/utils/line_chart.dart
46
46
  src/flutter/flet_charts/lib/src/utils/pie_chart.dart
47
47
  src/flutter/flet_charts/lib/src/utils/radar_chart.dart
48
48
  src/flutter/flet_charts/lib/src/utils/scatter_chart.dart
49
- tests/test_matplotlib_chart_canvas.py
49
+ tests/test_matplotlib_chart_canvas.py
50
+ tests/test_matplotlib_chart_receive_loop.py
@@ -0,0 +1 @@
1
+ flet==1.0.0.dev2
@@ -70,6 +70,7 @@ def _make_chart_standin():
70
70
  chart._waiting = False
71
71
  chart.img_count = 0
72
72
  chart._MatplotlibChart__image_mode = "full"
73
+ chart._handle_message = types.MethodType(MatplotlibChart._handle_message, chart)
73
74
  return chart, canvas
74
75
 
75
76
 
@@ -0,0 +1,122 @@
1
+ """Fault isolation in ``MatplotlibChart._receive_loop``.
2
+
3
+ The loop is the sole consumer of ``_receive_queue``, and it does more than
4
+ shuffle bytes: ``send_message`` hands a "draw" straight to Matplotlib's
5
+ canvas manager, so the figure renders *inside* this task. Any render error
6
+ — a bad artist, a font that fails to rasterize, a degenerate 3D view —
7
+ therefore surfaces here. Unguarded, it would end the loop: no frame would
8
+ ever be applied again, the chart would freeze with no visible error, and
9
+ the traceback would only appear much later as asyncio's "Task exception was
10
+ never retrieved" when the dead task is garbage-collected.
11
+
12
+ These tests drive the real ``_receive_loop`` with the failures injected at
13
+ the two points that can raise — the canvas apply, and the synchronous
14
+ Matplotlib render behind ``send_message``.
15
+ """
16
+
17
+ import asyncio
18
+ import types
19
+
20
+ from flet_charts.matplotlib_chart import MatplotlibChart
21
+
22
+
23
+ def _make_chart_standin():
24
+ """Minimal ``self`` for the real ``_receive_loop``: the queue/state
25
+ attrs ``build()`` would create, plus a fake canvas and ``send_message``
26
+ so a failure can be injected at either point."""
27
+ chart = types.SimpleNamespace()
28
+ chart._receive_queue = asyncio.Queue()
29
+ chart._waiting = False
30
+ chart.img_count = 0
31
+ chart._MatplotlibChart__image_mode = "full"
32
+ chart.applied = []
33
+ chart.sent = []
34
+
35
+ async def apply_raw_packet(packet):
36
+ if packet == b"boom":
37
+ raise RuntimeError("FT_Render_Glyph failed: raster overflow")
38
+ chart.applied.append(packet)
39
+
40
+ chart.mpl_canvas = types.SimpleNamespace(apply_raw_packet=apply_raw_packet)
41
+
42
+ def send_message(message):
43
+ chart.sent.append(message)
44
+ if chart.render_fails:
45
+ raise RuntimeError("FT_Render_Glyph failed: raster overflow")
46
+
47
+ chart.send_message = send_message
48
+ chart.render_fails = False
49
+ chart._handle_message = types.MethodType(MatplotlibChart._handle_message, chart)
50
+ return chart
51
+
52
+
53
+ def _feed_frame(chart, payload=b"frame"):
54
+ chart._receive_queue.put_nowait((True, ("raw", payload)))
55
+
56
+
57
+ async def _drain(chart):
58
+ """Run the real loop long enough to consume the queue, then stop it.
59
+
60
+ Returns whether the loop was still alive at that point — a loop killed
61
+ by an exception is `done()` before anything cancels it.
62
+ """
63
+ task = asyncio.create_task(MatplotlibChart._receive_loop(chart))
64
+ await asyncio.sleep(0.05)
65
+ alive = not task.done()
66
+ task.cancel()
67
+ return alive
68
+
69
+
70
+ def test_failing_render_does_not_kill_the_loop():
71
+ """A "draw" whose render raises must not stop frames that follow."""
72
+
73
+ async def scenario():
74
+ chart = _make_chart_standin()
75
+ chart.render_fails = True
76
+ chart._receive_queue.put_nowait((False, {"type": "draw"}))
77
+ chart.render_fails = False
78
+ _feed_frame(chart)
79
+ alive = await _drain(chart)
80
+ return chart.applied, chart._waiting, alive
81
+
82
+ applied, waiting, alive = asyncio.run(scenario())
83
+ assert applied == [b"frame"], "loop must keep applying frames after a render error"
84
+ assert not waiting, "a draw that raised must not leave the draw gate latched"
85
+ assert alive, "the loop task must still be running"
86
+
87
+
88
+ def test_failing_frame_apply_does_not_kill_the_loop():
89
+ """An error while applying one frame must not lose the next one."""
90
+
91
+ async def scenario():
92
+ chart = _make_chart_standin()
93
+ _feed_frame(chart, b"boom")
94
+ _feed_frame(chart, b"good")
95
+ await _drain(chart)
96
+ return chart.applied
97
+
98
+ assert asyncio.run(scenario()) == [b"good"]
99
+
100
+
101
+ def test_draw_gate_reopens_after_a_render_error():
102
+ """``_waiting`` gates draw requests; a failed draw never produces the
103
+ frame that clears it, so the loop must clear it itself — otherwise no
104
+ further draw is ever requested and the chart stays frozen."""
105
+
106
+ async def scenario():
107
+ chart = _make_chart_standin()
108
+ chart.render_fails = True
109
+ chart._receive_queue.put_nowait((False, {"type": "draw"}))
110
+ await _drain(chart)
111
+ first = list(chart.sent)
112
+
113
+ chart.render_fails = False
114
+ chart._receive_queue.put_nowait((False, {"type": "draw"}))
115
+ await _drain(chart)
116
+ return first, chart.sent
117
+
118
+ first, sent = asyncio.run(scenario())
119
+ assert first == [{"type": "draw"}]
120
+ assert sent == [{"type": "draw"}, {"type": "draw"}], (
121
+ "a later draw notification must still reach Matplotlib"
122
+ )
@@ -1 +0,0 @@
1
- flet==1.0.0.dev0