composeui 0.1.0__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.
Files changed (97) hide show
  1. composeui/__init__.py +11 -0
  2. composeui/core/__init__.py +0 -0
  3. composeui/core/application.py +368 -0
  4. composeui/core/chartcolors.py +23 -0
  5. composeui/core/chartimage.py +336 -0
  6. composeui/core/dataframesource.py +307 -0
  7. composeui/core/jobs.py +426 -0
  8. composeui/core/model.py +66 -0
  9. composeui/core/registry.py +66 -0
  10. composeui/core/reload.py +476 -0
  11. composeui/core/renderer.py +193 -0
  12. composeui/core/signal.py +358 -0
  13. composeui/core/sqlitemodel.py +1183 -0
  14. composeui/core/study.py +121 -0
  15. composeui/core/translation.py +221 -0
  16. composeui/core/translationcheck.py +254 -0
  17. composeui/core/view.py +5508 -0
  18. composeui/images/composeui.svg +15 -0
  19. composeui/locales/fr.json +73 -0
  20. composeui/mypy.py +46 -0
  21. composeui/py.typed +0 -0
  22. composeui/qtcore/__init__.py +0 -0
  23. composeui/qtcore/maprenderer.py +115 -0
  24. composeui/qtcore/qtapplication.py +222 -0
  25. composeui/qtcore/qtrenderer.py +2862 -0
  26. composeui/qtcore/widgets/__init__.py +0 -0
  27. composeui/qtcore/widgets/map.qml +123 -0
  28. composeui/qtcore/widgets/qchart.py +393 -0
  29. composeui/qtcore/widgets/qdatefield.py +101 -0
  30. composeui/qtcore/widgets/qexpander.py +60 -0
  31. composeui/qtcore/widgets/qfilepathfield.py +34 -0
  32. composeui/qtcore/widgets/qfiltertable.py +394 -0
  33. composeui/qtcore/widgets/qformdialog.py +64 -0
  34. composeui/qtcore/widgets/qformlabel.py +53 -0
  35. composeui/qtcore/widgets/qmap.py +68 -0
  36. composeui/qtcore/widgets/qmarkicon.py +37 -0
  37. composeui/qtcore/widgets/qmetric.py +44 -0
  38. composeui/qtcore/widgets/qplaintext.py +29 -0
  39. composeui/qtcore/widgets/qradiogroup.py +57 -0
  40. composeui/qtcore/widgets/qsidebar.py +116 -0
  41. composeui/qtcore/widgets/qsourcetable.py +216 -0
  42. composeui/qtcore/widgets/qsplitter.py +69 -0
  43. composeui/qtcore/widgets/qtexttable.py +703 -0
  44. composeui/qtcore/widgets/qvaluecombobox.py +41 -0
  45. composeui/qtcore/widgets/qvalueslider.py +64 -0
  46. composeui/qtcore/widgets/qwindowrelease.py +32 -0
  47. composeui/textualcore/__init__.py +0 -0
  48. composeui/textualcore/textualapplication.py +121 -0
  49. composeui/textualcore/textualrenderer.py +2893 -0
  50. composeui/textualcore/widgets/__init__.py +0 -0
  51. composeui/textualcore/widgets/composebutton.py +17 -0
  52. composeui/textualcore/widgets/composechart.py +212 -0
  53. composeui/textualcore/widgets/composecheckbox.py +35 -0
  54. composeui/textualcore/widgets/composecollapsible.py +41 -0
  55. composeui/textualcore/widgets/composecombobox.py +54 -0
  56. composeui/textualcore/widgets/composedateinput.py +325 -0
  57. composeui/textualcore/widgets/composedialog.py +90 -0
  58. composeui/textualcore/widgets/composefilepathinput.py +85 -0
  59. composeui/textualcore/widgets/composefiltertable.py +367 -0
  60. composeui/textualcore/widgets/composeformdialog.py +86 -0
  61. composeui/textualcore/widgets/composeinput.py +50 -0
  62. composeui/textualcore/widgets/composelist.py +20 -0
  63. composeui/textualcore/widgets/composemarkdown.py +17 -0
  64. composeui/textualcore/widgets/composemetric.py +60 -0
  65. composeui/textualcore/widgets/composeradioset.py +54 -0
  66. composeui/textualcore/widgets/composesidebar.py +111 -0
  67. composeui/textualcore/widgets/composeslider.py +144 -0
  68. composeui/textualcore/widgets/composesourcetable.py +288 -0
  69. composeui/textualcore/widgets/composesplitter.py +211 -0
  70. composeui/textualcore/widgets/composetabbedcontent.py +35 -0
  71. composeui/textualcore/widgets/composetable.py +476 -0
  72. composeui/textualcore/widgets/composetoolbar.py +62 -0
  73. composeui/textualcore/widgets/composetree.py +139 -0
  74. composeui/webcore/__init__.py +0 -0
  75. composeui/webcore/headers.py +57 -0
  76. composeui/webcore/icons.py +58 -0
  77. composeui/webcore/limits.py +55 -0
  78. composeui/webcore/origin.py +63 -0
  79. composeui/webcore/page.py +146 -0
  80. composeui/webcore/routes.py +303 -0
  81. composeui/webcore/session.py +436 -0
  82. composeui/webcore/static/app.css +2 -0
  83. composeui/webcore/static/cally.js +1312 -0
  84. composeui/webcore/static/daisyui.LICENSE +21 -0
  85. composeui/webcore/static/deckgl.LICENSE +19 -0
  86. composeui/webcore/static/deckgl.min.js +6423 -0
  87. composeui/webcore/static/htmx.min.js +1 -0
  88. composeui/webcore/static/page.js +1175 -0
  89. composeui/webcore/static/uPlot.LICENSE +21 -0
  90. composeui/webcore/static/uPlot.iife.min.js +2 -0
  91. composeui/webcore/webapplication.py +495 -0
  92. composeui/webcore/webrenderer.py +5113 -0
  93. composeui-0.1.0.dist-info/METADATA +222 -0
  94. composeui-0.1.0.dist-info/RECORD +97 -0
  95. composeui-0.1.0.dist-info/WHEEL +5 -0
  96. composeui-0.1.0.dist-info/licenses/LICENSE +9 -0
  97. composeui-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,358 @@
1
+ """The signals, which carry events between the views, the model and the app's code."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import functools
6
+ import inspect
7
+ import logging
8
+ import sys
9
+ import weakref
10
+ from collections.abc import Callable
11
+ from typing import TYPE_CHECKING, Any, Generic, TypeVar, Union, cast, overload
12
+
13
+ from typing_extensions import Protocol, TypeAliasType, TypeVarTuple, Unpack
14
+
15
+ if TYPE_CHECKING:
16
+ from collections.abc import Sequence
17
+ from weakref import ReferenceType
18
+
19
+ from qtpy.QtCore import SignalInstance
20
+
21
+ from composeui.core.model import Model
22
+ from composeui.core.view import MainView
23
+
24
+ __all__ = ["Signal"]
25
+
26
+
27
+ LOGGER = logging.getLogger("COMPOSEUI_SIGNAL")
28
+ _STREAM_HANDLER = logging.StreamHandler(sys.stdout)
29
+ _STREAM_HANDLER.setFormatter(logging.Formatter("%(asctime)s: %(message)s", "%Y-%m-%d %H:%M:%S"))
30
+ LOGGER.addHandler(_STREAM_HANDLER)
31
+
32
+ Ts = TypeVarTuple("Ts")
33
+ _Rest = TypeVarTuple("_Rest")
34
+ _FirstT = TypeVar("_FirstT")
35
+
36
+ _MainViewT_contra = TypeVar("_MainViewT_contra", contravariant=True)
37
+ _ModelT_contra = TypeVar("_ModelT_contra", contravariant=True)
38
+ _MainViewT = TypeVar("_MainViewT", bound="MainView")
39
+
40
+
41
+ class _MainViewCallback(Protocol[_MainViewT_contra, Unpack[Ts]]):
42
+ def __call__(self, *args: Unpack[Ts], main_view: _MainViewT_contra) -> None: ...
43
+
44
+
45
+ class _MainViewOnlyCallback(Protocol[_MainViewT_contra]):
46
+ def __call__(self, *, main_view: _MainViewT_contra) -> None: ...
47
+
48
+
49
+ class _ModelCallback(Protocol[_ModelT_contra, Unpack[Ts]]):
50
+ def __call__(self, *args: Unpack[Ts], model: _ModelT_contra) -> None: ...
51
+
52
+
53
+ class _BoundCallback(Protocol[_MainViewT_contra, _ModelT_contra, Unpack[Ts]]):
54
+ def __call__(
55
+ self, *args: Unpack[Ts], main_view: _MainViewT_contra, model: _ModelT_contra
56
+ ) -> None: ...
57
+
58
+
59
+ # `main_view` and `model` are typed `Any`: a signal can't know its app's classes, and a
60
+ # callback asks for those, not `MainView` and `Model` (`connect` still checks a main view)
61
+ CallbackT = TypeAliasType(
62
+ "CallbackT",
63
+ (
64
+ Union[
65
+ Callable[[Unpack[Ts]], None],
66
+ _MainViewCallback[Any, Unpack[Ts]],
67
+ _ModelCallback[Any, Unpack[Ts]],
68
+ _BoundCallback[Any, Any, Unpack[Ts]],
69
+ ]
70
+ ),
71
+ type_params=(Ts,),
72
+ )
73
+
74
+
75
+ def _positional_count(callback: Callable[..., None]) -> int | None:
76
+ """Get how many positional arguments `callback` takes; None for any number."""
77
+ try:
78
+ params = inspect.signature(callback).parameters.values()
79
+ except (TypeError, ValueError):
80
+ return None
81
+ positional = (inspect.Parameter.POSITIONAL_ONLY, inspect.Parameter.POSITIONAL_OR_KEYWORD)
82
+ if any(param.kind is inspect.Parameter.VAR_POSITIONAL for param in params):
83
+ return None
84
+ return sum(
85
+ param.kind in positional and param.name not in ("main_view", "model") for param in params
86
+ )
87
+
88
+
89
+ class Signal(Generic[Unpack[Ts]]):
90
+ """An event that a view, a model or the app emits, with the callbacks connected to it.
91
+
92
+ `connect` adds a callback, and `emit` calls each callback with the signal's arguments. A
93
+ callback with a ``main_view`` or ``model`` parameter gets it by name (see `connect`).
94
+ """
95
+
96
+ def __init__(self) -> None:
97
+ self._name = ""
98
+ self._qt_signal = _QtSignal()
99
+ self._callbacks: list[Callable[[Unpack[Ts]], None]] = [self._log_signal]
100
+ self._logger = LOGGER
101
+ self._main_view: ReferenceType[MainView] | None = None
102
+ self._model: ReferenceType[Model] | None = None
103
+
104
+ def set_name(self, name: str) -> None:
105
+ self._name = name
106
+
107
+ def set_main_view(self, main_view: MainView) -> None:
108
+ self._main_view = weakref.ref(main_view)
109
+ for callback in self._callbacks:
110
+ if isinstance(callback, InjectedCallback):
111
+ callback.set_main_view(self._main_view)
112
+
113
+ def set_model(self, model: Model) -> None:
114
+ self._model = weakref.ref(model)
115
+ for callback in self._callbacks:
116
+ if isinstance(callback, InjectedCallback):
117
+ callback.set_model(self._model)
118
+
119
+ def add_qt_signal(self, signal: SignalInstance) -> None:
120
+ """Call this signal's callbacks when the Qt signal `signal` emits too."""
121
+ self._qt_signal.add_signal(signal, self._callbacks)
122
+
123
+ @overload
124
+ def connect(self, callback: Callable[[Unpack[Ts]], None], *, now: bool = False) -> None: ...
125
+ @overload
126
+ def connect(
127
+ self, callback: _MainViewCallback[_MainViewT, Unpack[Ts]], *, now: bool = False
128
+ ) -> None: ...
129
+ @overload
130
+ def connect(self, callback: _ModelCallback[Any, Unpack[Ts]], *, now: bool = False) -> None: ...
131
+ @overload
132
+ def connect(
133
+ self, callback: _BoundCallback[_MainViewT, Any, Unpack[Ts]], *, now: bool = False
134
+ ) -> None: ...
135
+ # a callback taking none of the arguments
136
+ @overload
137
+ def connect(self, callback: Callable[[], None], *, now: bool = False) -> None: ...
138
+ @overload
139
+ def connect(
140
+ self, callback: _MainViewOnlyCallback[_MainViewT], *, now: bool = False
141
+ ) -> None: ...
142
+ @overload
143
+ def connect(self, callback: _ModelCallback[Any], *, now: bool = False) -> None: ...
144
+ @overload
145
+ def connect(self, callback: _BoundCallback[_MainViewT, Any], *, now: bool = False) -> None: ...
146
+ @overload
147
+ def connect(
148
+ self: Signal[_FirstT, Unpack[_Rest]],
149
+ callback: Callable[[_FirstT], None],
150
+ *,
151
+ now: bool = False,
152
+ ) -> None: ...
153
+ def connect(self, callback: Callable[..., None], *, now: bool = False) -> None:
154
+ """Call `callback` on each emit, and once right away too if `now`.
155
+
156
+ The callback gets the `main_view` and the `model` it asks for by name. It gets only as
157
+ many of the signal's arguments as it takes, the first ones, and none when called `now`.
158
+ """
159
+ injected_callback = self._inject_callback(callback)
160
+ self._callbacks.append(injected_callback)
161
+ self._qt_signal.connect(injected_callback)
162
+ if now:
163
+ cast("Callable[[], None]", injected_callback)()
164
+
165
+ def disconnect(self) -> None:
166
+ self._qt_signal.disconnect(self._callbacks)
167
+ self._callbacks = [self._log_signal]
168
+ self._qt_signal.connect(self._log_signal)
169
+
170
+ def emit(self, *args: Unpack[Ts]) -> None:
171
+ for callback in self._callbacks:
172
+ callback(*args)
173
+
174
+ def _log_signal(self, *args: Unpack[Ts]) -> None:
175
+ log_msgs = ["EMIT SIGNAL %s"]
176
+ log_args = [self._name]
177
+ nb_callbacks = 0
178
+ for callback in self._callbacks:
179
+ if callback != self._log_signal:
180
+ callback_name = self.get_function_name(callback)
181
+ callback_file = self.get_function_file(callback)
182
+ if len(args) > 0:
183
+ log_msgs.append(" -> CALL %s(%s)\n %s")
184
+ else:
185
+ log_msgs.append(" -> CALL %s()\n %s")
186
+ log_args.append(callback_name)
187
+ if len(args) > 0:
188
+ log_args.append(",".join(repr(arg) for arg in args))
189
+ log_args.append(callback_file)
190
+ nb_callbacks += 1
191
+ if nb_callbacks > 0:
192
+ self._logger.debug("\n".join(log_msgs), *log_args)
193
+
194
+ def _inject_callback(
195
+ self, callback: Callable[..., None]
196
+ ) -> (
197
+ Callable[[Unpack[Ts]], None]
198
+ | InjectedCallback[Unpack[Ts]]
199
+ | _FirstArgumentsCallback[Unpack[Ts]]
200
+ ):
201
+ requested_bounds = self._requested_bounds(callback)
202
+ if len(requested_bounds) > 0:
203
+ return InjectedCallback[Unpack[Ts]](
204
+ callback=callback,
205
+ requested_bounds=requested_bounds,
206
+ optional_bounds=self._optional_bounds(callback),
207
+ main_view=self._main_view,
208
+ model=self._model,
209
+ )
210
+ count = _positional_count(callback)
211
+ if count is not None:
212
+ return _FirstArgumentsCallback[Unpack[Ts]](callback, count)
213
+ return cast("Callable[[Unpack[Ts]], None]", callback)
214
+
215
+ @staticmethod
216
+ def _requested_bounds(callback: Callable[..., None]) -> frozenset[str]:
217
+ names = ("main_view", "model")
218
+ try:
219
+ params = inspect.signature(callback).parameters
220
+ except (TypeError, ValueError):
221
+ return frozenset()
222
+ return frozenset(name for name in names if name in params)
223
+
224
+ @staticmethod
225
+ def _optional_bounds(callback: Callable[..., None]) -> frozenset[str]:
226
+ """Get the bounds `callback` has a default for, left out when there is none to give.
227
+
228
+ Asked once `_requested_bounds` found some: its signature can be read.
229
+ """
230
+ params = inspect.signature(callback).parameters
231
+ return frozenset(
232
+ name
233
+ for name in ("main_view", "model")
234
+ if name in params and params[name].default is not inspect.Parameter.empty
235
+ )
236
+
237
+ @staticmethod
238
+ def get_function_name(callback: CallbackT[Any]) -> str:
239
+ if isinstance(callback, (InjectedCallback, _FirstArgumentsCallback)):
240
+ wrapped = callback.get_callback()
241
+ return "<collected>" if wrapped is None else Signal.get_function_name(wrapped) # type: ignore[arg-type]
242
+ if isinstance(callback, functools.partial):
243
+ inner = Signal.get_function_name(callback.func)
244
+ args = list(map(repr, callback.args))
245
+ kwargs = [f"{n}={type(arg).__name__}" for n, arg in callback.keywords.items()]
246
+ return f"{inner}({', '.join(args + kwargs)})"
247
+ name = getattr(callback, "__qualname__", None) or getattr(callback, "__name__", None)
248
+ return name or type(callback).__name__
249
+
250
+ @staticmethod
251
+ def get_function_file(callback: CallbackT[Any]) -> str:
252
+ if isinstance(callback, (InjectedCallback, _FirstArgumentsCallback)):
253
+ wrapped = callback.get_callback()
254
+ return "" if wrapped is None else Signal.get_function_file(wrapped) # type: ignore[arg-type]
255
+ if isinstance(callback, functools.partial):
256
+ return Signal.get_function_file(callback.func)
257
+ code = getattr(callback, "__code__", None)
258
+ if code is None:
259
+ # built-ins (e.g. `list.append`) and callable instances have no code object
260
+ return repr(callback)
261
+ return f'File "{code.co_filename}", line {code.co_firstlineno}'
262
+
263
+
264
+ if TYPE_CHECKING:
265
+ SignalNoArgs = Signal[()]
266
+ else:
267
+ SignalNoArgs = Signal
268
+
269
+
270
+ class _QtSignal:
271
+ def __init__(self) -> None:
272
+ self._qt_signals: list[SignalInstance] = []
273
+
274
+ def add_signal(self, signal: SignalInstance, callbacks: Sequence[Callable[..., None]]) -> None:
275
+ """Add the Qt signal `signal`, and connect `callbacks` to it."""
276
+ self._qt_signals.append(signal)
277
+ for callback in callbacks:
278
+ signal.connect(callback)
279
+
280
+ def connect(self, callback: Callable[..., None]) -> None:
281
+ for qt_signal in self._qt_signals:
282
+ qt_signal.connect(callback)
283
+
284
+ def disconnect(self, callbacks: Sequence[Callable[..., None]]) -> None:
285
+ for qt_signal in self._qt_signals:
286
+ for callback in callbacks:
287
+ qt_signal.disconnect(callback)
288
+
289
+
290
+ class _FirstArgumentsCallback(Generic[Unpack[Ts]]):
291
+ """A callback taking fewer positional arguments than its signal emits: the first ones."""
292
+
293
+ def __init__(self, callback: Callable[..., None], count: int) -> None:
294
+ self._callback = callback
295
+ self._count = count
296
+
297
+ def get_callback(self) -> Callable[..., None]:
298
+ return self._callback
299
+
300
+ def __call__(self, *args: Unpack[Ts]) -> None:
301
+ self._callback(*args[: self._count])
302
+
303
+
304
+ class InjectedCallback(Generic[Unpack[Ts]]):
305
+ """A callback that gets the ``main_view`` and ``model`` it asks for by name."""
306
+
307
+ def __init__(
308
+ self,
309
+ *,
310
+ callback: Callable[..., None],
311
+ requested_bounds: frozenset[str],
312
+ optional_bounds: frozenset[str] = frozenset(),
313
+ main_view: ReferenceType[MainView] | None,
314
+ model: ReferenceType[Model] | None,
315
+ ) -> None:
316
+ # a view's own method stays weak, so its signals don't keep the view alive; a function
317
+ # or a partial (e.g. `Application.connect`'s) has no owner that would hold it
318
+ self._callback: Callable[[], Callable[..., None] | None]
319
+ if inspect.ismethod(callback):
320
+ self._callback = cast("Callable[[], Any]", weakref.WeakMethod(callback))
321
+ else:
322
+ self._callback = lambda: callback
323
+ self._requested_bounds = requested_bounds
324
+ self._optional_bounds = optional_bounds
325
+ self._main_view = main_view
326
+ self._model = model
327
+ #: how many of the signal's arguments the callback takes, the first ones; None for all
328
+ self._count = _positional_count(callback)
329
+
330
+ def set_main_view(self, main_view: ReferenceType[MainView]) -> None:
331
+ self._main_view = main_view
332
+
333
+ def set_model(self, model: ReferenceType[Model]) -> None:
334
+ self._model = model
335
+
336
+ def get_callback(self) -> Callable[..., None] | None:
337
+ """Get the callback; None once it's a method of a view collected since."""
338
+ return self._callback()
339
+
340
+ def __call__(self, *args: Unpack[Ts]) -> None:
341
+ callback = self._callback()
342
+ if callback is None:
343
+ return None # its view is gone: as Qt, which drops a deleted receiver's slot
344
+ kwargs: dict[str, MainView | Model] = {}
345
+ if "main_view" in self._requested_bounds:
346
+ main_view = None if self._main_view is None else self._main_view()
347
+ if main_view is None:
348
+ msg = "The main view is missing."
349
+ raise ValueError(msg)
350
+ kwargs["main_view"] = main_view
351
+ if "model" in self._requested_bounds:
352
+ model = None if self._model is None else self._model()
353
+ if model is not None:
354
+ kwargs["model"] = model
355
+ elif "model" not in self._optional_bounds:
356
+ msg = "The model is missing."
357
+ raise ValueError(msg)
358
+ return callback(*args[: self._count], **kwargs)