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.
- composeui/__init__.py +11 -0
- composeui/core/__init__.py +0 -0
- composeui/core/application.py +368 -0
- composeui/core/chartcolors.py +23 -0
- composeui/core/chartimage.py +336 -0
- composeui/core/dataframesource.py +307 -0
- composeui/core/jobs.py +426 -0
- composeui/core/model.py +66 -0
- composeui/core/registry.py +66 -0
- composeui/core/reload.py +476 -0
- composeui/core/renderer.py +193 -0
- composeui/core/signal.py +358 -0
- composeui/core/sqlitemodel.py +1183 -0
- composeui/core/study.py +121 -0
- composeui/core/translation.py +221 -0
- composeui/core/translationcheck.py +254 -0
- composeui/core/view.py +5508 -0
- composeui/images/composeui.svg +15 -0
- composeui/locales/fr.json +73 -0
- composeui/mypy.py +46 -0
- composeui/py.typed +0 -0
- composeui/qtcore/__init__.py +0 -0
- composeui/qtcore/maprenderer.py +115 -0
- composeui/qtcore/qtapplication.py +222 -0
- composeui/qtcore/qtrenderer.py +2862 -0
- composeui/qtcore/widgets/__init__.py +0 -0
- composeui/qtcore/widgets/map.qml +123 -0
- composeui/qtcore/widgets/qchart.py +393 -0
- composeui/qtcore/widgets/qdatefield.py +101 -0
- composeui/qtcore/widgets/qexpander.py +60 -0
- composeui/qtcore/widgets/qfilepathfield.py +34 -0
- composeui/qtcore/widgets/qfiltertable.py +394 -0
- composeui/qtcore/widgets/qformdialog.py +64 -0
- composeui/qtcore/widgets/qformlabel.py +53 -0
- composeui/qtcore/widgets/qmap.py +68 -0
- composeui/qtcore/widgets/qmarkicon.py +37 -0
- composeui/qtcore/widgets/qmetric.py +44 -0
- composeui/qtcore/widgets/qplaintext.py +29 -0
- composeui/qtcore/widgets/qradiogroup.py +57 -0
- composeui/qtcore/widgets/qsidebar.py +116 -0
- composeui/qtcore/widgets/qsourcetable.py +216 -0
- composeui/qtcore/widgets/qsplitter.py +69 -0
- composeui/qtcore/widgets/qtexttable.py +703 -0
- composeui/qtcore/widgets/qvaluecombobox.py +41 -0
- composeui/qtcore/widgets/qvalueslider.py +64 -0
- composeui/qtcore/widgets/qwindowrelease.py +32 -0
- composeui/textualcore/__init__.py +0 -0
- composeui/textualcore/textualapplication.py +121 -0
- composeui/textualcore/textualrenderer.py +2893 -0
- composeui/textualcore/widgets/__init__.py +0 -0
- composeui/textualcore/widgets/composebutton.py +17 -0
- composeui/textualcore/widgets/composechart.py +212 -0
- composeui/textualcore/widgets/composecheckbox.py +35 -0
- composeui/textualcore/widgets/composecollapsible.py +41 -0
- composeui/textualcore/widgets/composecombobox.py +54 -0
- composeui/textualcore/widgets/composedateinput.py +325 -0
- composeui/textualcore/widgets/composedialog.py +90 -0
- composeui/textualcore/widgets/composefilepathinput.py +85 -0
- composeui/textualcore/widgets/composefiltertable.py +367 -0
- composeui/textualcore/widgets/composeformdialog.py +86 -0
- composeui/textualcore/widgets/composeinput.py +50 -0
- composeui/textualcore/widgets/composelist.py +20 -0
- composeui/textualcore/widgets/composemarkdown.py +17 -0
- composeui/textualcore/widgets/composemetric.py +60 -0
- composeui/textualcore/widgets/composeradioset.py +54 -0
- composeui/textualcore/widgets/composesidebar.py +111 -0
- composeui/textualcore/widgets/composeslider.py +144 -0
- composeui/textualcore/widgets/composesourcetable.py +288 -0
- composeui/textualcore/widgets/composesplitter.py +211 -0
- composeui/textualcore/widgets/composetabbedcontent.py +35 -0
- composeui/textualcore/widgets/composetable.py +476 -0
- composeui/textualcore/widgets/composetoolbar.py +62 -0
- composeui/textualcore/widgets/composetree.py +139 -0
- composeui/webcore/__init__.py +0 -0
- composeui/webcore/headers.py +57 -0
- composeui/webcore/icons.py +58 -0
- composeui/webcore/limits.py +55 -0
- composeui/webcore/origin.py +63 -0
- composeui/webcore/page.py +146 -0
- composeui/webcore/routes.py +303 -0
- composeui/webcore/session.py +436 -0
- composeui/webcore/static/app.css +2 -0
- composeui/webcore/static/cally.js +1312 -0
- composeui/webcore/static/daisyui.LICENSE +21 -0
- composeui/webcore/static/deckgl.LICENSE +19 -0
- composeui/webcore/static/deckgl.min.js +6423 -0
- composeui/webcore/static/htmx.min.js +1 -0
- composeui/webcore/static/page.js +1175 -0
- composeui/webcore/static/uPlot.LICENSE +21 -0
- composeui/webcore/static/uPlot.iife.min.js +2 -0
- composeui/webcore/webapplication.py +495 -0
- composeui/webcore/webrenderer.py +5113 -0
- composeui-0.1.0.dist-info/METADATA +222 -0
- composeui-0.1.0.dist-info/RECORD +97 -0
- composeui-0.1.0.dist-info/WHEEL +5 -0
- composeui-0.1.0.dist-info/licenses/LICENSE +9 -0
- composeui-0.1.0.dist-info/top_level.txt +1 -0
composeui/__init__.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from importlib.metadata import version
|
|
4
|
+
|
|
5
|
+
from composeui.core.application import LOGGER
|
|
6
|
+
from composeui.core.signal import LOGGER as SIGNAL_LOGGER
|
|
7
|
+
|
|
8
|
+
__all__ = ["LOGGER", "SIGNAL_LOGGER", "__version__"]
|
|
9
|
+
|
|
10
|
+
#: the version of the installed package, from its tag (setuptools-scm)
|
|
11
|
+
__version__ = version("composeui")
|
|
File without changes
|
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
"""The app: it builds the main view, wires its signals and runs it on a backend (`run`)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import functools
|
|
7
|
+
import importlib
|
|
8
|
+
import importlib.util
|
|
9
|
+
import inspect
|
|
10
|
+
import logging
|
|
11
|
+
import os
|
|
12
|
+
import sys
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import TYPE_CHECKING, Generic, Protocol, TypeVar, cast
|
|
15
|
+
|
|
16
|
+
from typing_extensions import TypedDict, TypeVarTuple, Unpack, overload, override
|
|
17
|
+
|
|
18
|
+
from composeui.core.registry import RendererType
|
|
19
|
+
from composeui.core.signal import LOGGER as SIGNAL_LOGGER
|
|
20
|
+
from composeui.core.signal import Signal
|
|
21
|
+
from composeui.core.study import open_study
|
|
22
|
+
from composeui.core.view import set_references
|
|
23
|
+
|
|
24
|
+
if TYPE_CHECKING:
|
|
25
|
+
from collections.abc import Callable, Sequence
|
|
26
|
+
|
|
27
|
+
from composeui.core.model import Model
|
|
28
|
+
from composeui.core.signal import _BoundCallback, _MainViewCallback, _ModelCallback
|
|
29
|
+
from composeui.core.view import MainView, View
|
|
30
|
+
|
|
31
|
+
__all__ = ["Application", "ModelApplication", "run"]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
LOGGER = logging.getLogger("COMPOSEUI")
|
|
35
|
+
_STREAM_HANDLER = logging.StreamHandler(sys.stdout)
|
|
36
|
+
_STREAM_HANDLER.setFormatter(logging.Formatter("%(asctime)s: %(message)s", "%Y-%m-%d %H:%M:%S"))
|
|
37
|
+
LOGGER.addHandler(_STREAM_HANDLER)
|
|
38
|
+
|
|
39
|
+
# T = TypeVar("T", bound=MainView)
|
|
40
|
+
Ts = TypeVarTuple("Ts")
|
|
41
|
+
|
|
42
|
+
_ViewT_contra = TypeVar("_ViewT_contra", contravariant=True)
|
|
43
|
+
_MainViewT_contra = TypeVar("_MainViewT_contra", contravariant=True)
|
|
44
|
+
_ModelT_contra = TypeVar("_ModelT_contra", contravariant=True)
|
|
45
|
+
ViewT = TypeVar("ViewT", bound="View")
|
|
46
|
+
MainViewT = TypeVar("MainViewT", bound="MainView")
|
|
47
|
+
ModelT = TypeVar("ModelT", bound="Model")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
_ViewTD = TypeVar("_ViewTD")
|
|
51
|
+
_MainViewTD = TypeVar("_MainViewTD")
|
|
52
|
+
_ModelTD = TypeVar("_ModelTD")
|
|
53
|
+
_TsD = TypeVarTuple("_TsD")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class AvailableBounds(TypedDict, Generic[_ViewT_contra, _MainViewT_contra]):
|
|
57
|
+
"""The arguments `Application.connect` can give a callback by name."""
|
|
58
|
+
|
|
59
|
+
view: _ViewT_contra
|
|
60
|
+
main_view: _MainViewT_contra
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class AvailableModelBounds(TypedDict, Generic[_ViewT_contra, _MainViewT_contra, _ModelT_contra]):
|
|
64
|
+
"""The arguments `ModelApplication.connect` can give a callback by name."""
|
|
65
|
+
|
|
66
|
+
view: _ViewT_contra
|
|
67
|
+
main_view: _MainViewT_contra
|
|
68
|
+
model: _ModelT_contra
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class _ViewOnlyCallback(Protocol[_ViewT_contra, Unpack[Ts]]):
|
|
72
|
+
def __call__(self, *args: Unpack[Ts], view: _ViewT_contra) -> None: ...
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class _ViewMainCallback(Protocol[_ViewT_contra, _MainViewT_contra, Unpack[Ts]]):
|
|
76
|
+
def __call__(
|
|
77
|
+
self, *args: Unpack[Ts], view: _ViewT_contra, main_view: _MainViewT_contra
|
|
78
|
+
) -> None: ...
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class _ViewModelCallback(Protocol[_ViewT_contra, _ModelT_contra, Unpack[Ts]]):
|
|
82
|
+
def __call__(self, *args: Unpack[Ts], view: _ViewT_contra, model: _ModelT_contra) -> None: ...
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class _ViewMainModelCallback(
|
|
86
|
+
Protocol[_ViewT_contra, _MainViewT_contra, _ModelT_contra, Unpack[Ts]]
|
|
87
|
+
):
|
|
88
|
+
def __call__(
|
|
89
|
+
self,
|
|
90
|
+
*args: Unpack[Ts],
|
|
91
|
+
view: _ViewT_contra,
|
|
92
|
+
main_view: _MainViewT_contra,
|
|
93
|
+
model: _ModelT_contra,
|
|
94
|
+
) -> None: ...
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class _ConnectDecorator(Protocol[_ViewTD, _MainViewTD, Unpack[_TsD]]):
|
|
98
|
+
@overload
|
|
99
|
+
def __call__(
|
|
100
|
+
self, callback: Callable[[Unpack[_TsD]], None]
|
|
101
|
+
) -> Callable[[Unpack[_TsD]], None]: ...
|
|
102
|
+
@overload
|
|
103
|
+
def __call__(
|
|
104
|
+
self, callback: _MainViewCallback[_MainViewTD, Unpack[_TsD]]
|
|
105
|
+
) -> _MainViewCallback[_MainViewTD, Unpack[_TsD]]: ...
|
|
106
|
+
@overload
|
|
107
|
+
def __call__(
|
|
108
|
+
self, callback: _ViewOnlyCallback[_ViewTD, Unpack[_TsD]]
|
|
109
|
+
) -> _ViewOnlyCallback[_ViewTD, Unpack[_TsD]]: ...
|
|
110
|
+
@overload
|
|
111
|
+
def __call__(
|
|
112
|
+
self, callback: _ViewMainCallback[_ViewTD, _MainViewTD, Unpack[_TsD]]
|
|
113
|
+
) -> _ViewMainCallback[_ViewTD, _MainViewTD, Unpack[_TsD]]: ...
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class _ConnectModelDecorator(Protocol[_ViewTD, _MainViewTD, _ModelTD, Unpack[_TsD]]):
|
|
117
|
+
@overload
|
|
118
|
+
def __call__(
|
|
119
|
+
self, callback: Callable[[Unpack[_TsD]], None]
|
|
120
|
+
) -> Callable[[Unpack[_TsD]], None]: ...
|
|
121
|
+
@overload
|
|
122
|
+
def __call__(
|
|
123
|
+
self, callback: _MainViewCallback[_MainViewTD, Unpack[_TsD]]
|
|
124
|
+
) -> _MainViewCallback[_MainViewTD, Unpack[_TsD]]: ...
|
|
125
|
+
@overload
|
|
126
|
+
def __call__(
|
|
127
|
+
self, callback: _ModelCallback[_ModelTD, Unpack[_TsD]]
|
|
128
|
+
) -> _ModelCallback[_ModelTD, Unpack[_TsD]]: ...
|
|
129
|
+
@overload
|
|
130
|
+
def __call__(
|
|
131
|
+
self, callback: _BoundCallback[_MainViewTD, _ModelTD, Unpack[_TsD]]
|
|
132
|
+
) -> _BoundCallback[_MainViewTD, _ModelTD, Unpack[_TsD]]: ...
|
|
133
|
+
@overload
|
|
134
|
+
def __call__(
|
|
135
|
+
self, callback: _ViewOnlyCallback[_ViewTD, Unpack[_TsD]]
|
|
136
|
+
) -> _ViewOnlyCallback[_ViewTD, Unpack[_TsD]]: ...
|
|
137
|
+
@overload
|
|
138
|
+
def __call__(
|
|
139
|
+
self, callback: _ViewMainCallback[_ViewTD, _MainViewTD, Unpack[_TsD]]
|
|
140
|
+
) -> _ViewMainCallback[_ViewTD, _MainViewTD, Unpack[_TsD]]: ...
|
|
141
|
+
@overload
|
|
142
|
+
def __call__(
|
|
143
|
+
self, callback: _ViewModelCallback[_ViewTD, _ModelTD, Unpack[_TsD]]
|
|
144
|
+
) -> _ViewModelCallback[_ViewTD, _ModelTD, Unpack[_TsD]]: ...
|
|
145
|
+
@overload
|
|
146
|
+
def __call__(
|
|
147
|
+
self, callback: _ViewMainModelCallback[_ViewTD, _MainViewTD, _ModelTD, Unpack[_TsD]]
|
|
148
|
+
) -> _ViewMainModelCallback[_ViewTD, _MainViewTD, _ModelTD, Unpack[_TsD]]: ...
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _add_reference_to_signals(main_view: MainView, model: Model | None) -> None:
|
|
152
|
+
set_references(main_view, main_view, model)
|
|
153
|
+
if model is not None:
|
|
154
|
+
main_view.jobs.set_model(model)
|
|
155
|
+
for signal in model.signals:
|
|
156
|
+
signal.set_main_view(main_view)
|
|
157
|
+
signal.set_model(model)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
class Application(Generic[MainViewT]):
|
|
161
|
+
"""An app showing the main view `build_main_view` builds; each backend has its own.
|
|
162
|
+
|
|
163
|
+
The app builds its main view itself, so every backend takes the same arguments: a web
|
|
164
|
+
app builds one per browser session.
|
|
165
|
+
"""
|
|
166
|
+
|
|
167
|
+
def __init__(self, *, build_main_view: Callable[[], MainViewT]) -> None:
|
|
168
|
+
self._main_view = build_main_view()
|
|
169
|
+
self._args: argparse.Namespace | None = None
|
|
170
|
+
self._parser = argparse.ArgumentParser()
|
|
171
|
+
self._parser.add_argument("--debug", help="Run the app in debug mode", action="store_true")
|
|
172
|
+
self._parser.add_argument(
|
|
173
|
+
"-f", metavar="FILE", help="Open the app with the FILE study", type=Path
|
|
174
|
+
)
|
|
175
|
+
self._parser.add_argument("--log", help="Display the debug Log.", action="store_true")
|
|
176
|
+
self._parser.add_argument(
|
|
177
|
+
"--log-signals", help="Log all the emitted signals.", action="store_true"
|
|
178
|
+
)
|
|
179
|
+
self._parser.add_argument(
|
|
180
|
+
"--language", metavar="LANGUAGE", help="Show the app in LANGUAGE, e.g. fr."
|
|
181
|
+
)
|
|
182
|
+
self._add_reference_to_signals()
|
|
183
|
+
|
|
184
|
+
def _add_reference_to_signals(self) -> None:
|
|
185
|
+
_add_reference_to_signals(self._main_view, None)
|
|
186
|
+
|
|
187
|
+
@property
|
|
188
|
+
def main_view(self) -> MainViewT:
|
|
189
|
+
return self._main_view
|
|
190
|
+
|
|
191
|
+
def connect(
|
|
192
|
+
self, *, view: ViewT, signals: Sequence[Signal[Unpack[Ts]]]
|
|
193
|
+
) -> _ConnectDecorator[ViewT, MainViewT, Unpack[Ts]]:
|
|
194
|
+
def wrapper(callback: Callable[..., None]) -> Callable[..., None]:
|
|
195
|
+
available_bounds: AvailableBounds[ViewT, MainViewT] = {
|
|
196
|
+
"view": view,
|
|
197
|
+
"main_view": self._main_view,
|
|
198
|
+
}
|
|
199
|
+
requested_bounds = self._requested_kwargs(callback, tuple(available_bounds))
|
|
200
|
+
bound = functools.partial(
|
|
201
|
+
callback, **{k: v for k, v in available_bounds.items() if k in requested_bounds}
|
|
202
|
+
)
|
|
203
|
+
new_callback = cast("Callable[[Unpack[Ts]], None]", bound)
|
|
204
|
+
for s in signals:
|
|
205
|
+
s.connect(new_callback)
|
|
206
|
+
return callback
|
|
207
|
+
|
|
208
|
+
return cast("_ConnectDecorator[ViewT, MainViewT, Unpack[Ts]]", wrapper)
|
|
209
|
+
|
|
210
|
+
def render(self, renderer_type: RendererType) -> None:
|
|
211
|
+
self._main_view.is_visible = True
|
|
212
|
+
self._main_view.render(renderer_type)
|
|
213
|
+
|
|
214
|
+
def run(self) -> None:
|
|
215
|
+
self._args = self._parse_arguments()
|
|
216
|
+
if self._args.log or self._args.debug:
|
|
217
|
+
LOGGER.setLevel(level=logging.DEBUG)
|
|
218
|
+
else:
|
|
219
|
+
LOGGER.setLevel(level=logging.INFO)
|
|
220
|
+
if self._args.log_signals or self._args.debug:
|
|
221
|
+
SIGNAL_LOGGER.setLevel(level=logging.DEBUG)
|
|
222
|
+
else:
|
|
223
|
+
SIGNAL_LOGGER.setLevel(level=logging.INFO)
|
|
224
|
+
if self._args.language is not None:
|
|
225
|
+
self.main_view.set_language(self._args.language)
|
|
226
|
+
if self._args.f is not None:
|
|
227
|
+
open_study(self.main_view, self._study_model(), self._args.f)
|
|
228
|
+
self.main_view.apply_arguments(self._args)
|
|
229
|
+
|
|
230
|
+
def _study_model(self) -> Model | None:
|
|
231
|
+
"""Get the model opened, saved and started over with the views: none here."""
|
|
232
|
+
return None
|
|
233
|
+
|
|
234
|
+
def _parse_arguments(self) -> argparse.Namespace:
|
|
235
|
+
# last: after the backend's own, which a subclass adds once built
|
|
236
|
+
type(self.main_view).add_arguments(self._parser)
|
|
237
|
+
args = self._parser.parse_args()
|
|
238
|
+
if (
|
|
239
|
+
args.f is not None
|
|
240
|
+
and isinstance(args.f, Path)
|
|
241
|
+
and args.f.is_file()
|
|
242
|
+
and args.f.suffix != self.main_view.extension
|
|
243
|
+
):
|
|
244
|
+
msg = (
|
|
245
|
+
f"Invalid file extension: '{args.f.suffix}'. "
|
|
246
|
+
f"Expected extension: '{self.main_view.extension}'. "
|
|
247
|
+
"Please provide a file with the correct extension."
|
|
248
|
+
)
|
|
249
|
+
raise ValueError(msg)
|
|
250
|
+
return args
|
|
251
|
+
|
|
252
|
+
@staticmethod
|
|
253
|
+
def _requested_kwargs(callback: object, names: tuple[str, ...]) -> frozenset[str]:
|
|
254
|
+
try:
|
|
255
|
+
params = inspect.signature(callback).parameters # type: ignore[arg-type]
|
|
256
|
+
except (TypeError, ValueError):
|
|
257
|
+
return frozenset()
|
|
258
|
+
return frozenset(name for name in names if name in params)
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
class ModelApplication(Application[MainViewT], Generic[MainViewT, ModelT]):
|
|
262
|
+
"""An app with a model beside its main view, given to the callbacks asking for `model`.
|
|
263
|
+
|
|
264
|
+
`setup_model` wires them once both are built, e.g. to refresh views when the model
|
|
265
|
+
changes.
|
|
266
|
+
"""
|
|
267
|
+
|
|
268
|
+
def __init__(
|
|
269
|
+
self,
|
|
270
|
+
*,
|
|
271
|
+
build_main_view: Callable[[], MainViewT],
|
|
272
|
+
build_model: Callable[[], ModelT],
|
|
273
|
+
setup_model: Callable[[MainViewT, ModelT], None] | None = None,
|
|
274
|
+
) -> None:
|
|
275
|
+
# first: the base class gives it to the signals
|
|
276
|
+
self._model = build_model()
|
|
277
|
+
super().__init__(build_main_view=build_main_view)
|
|
278
|
+
if setup_model is not None:
|
|
279
|
+
setup_model(self._main_view, self._model)
|
|
280
|
+
|
|
281
|
+
@override
|
|
282
|
+
def _add_reference_to_signals(self) -> None:
|
|
283
|
+
_add_reference_to_signals(self._main_view, self._model)
|
|
284
|
+
|
|
285
|
+
@property
|
|
286
|
+
def model(self) -> ModelT:
|
|
287
|
+
return self._model
|
|
288
|
+
|
|
289
|
+
@override
|
|
290
|
+
def _study_model(self) -> ModelT:
|
|
291
|
+
return self._model
|
|
292
|
+
|
|
293
|
+
def connect(
|
|
294
|
+
self, *, view: ViewT, signals: Sequence[Signal[Unpack[Ts]]]
|
|
295
|
+
) -> _ConnectModelDecorator[ViewT, MainViewT, ModelT, Unpack[Ts]]:
|
|
296
|
+
def wrapper(callback: Callable[..., None]) -> Callable[..., None]:
|
|
297
|
+
available_bounds: AvailableModelBounds[ViewT, MainViewT, ModelT] = {
|
|
298
|
+
"view": view,
|
|
299
|
+
"main_view": self._main_view,
|
|
300
|
+
"model": self._model,
|
|
301
|
+
}
|
|
302
|
+
requested_bounds = self._requested_kwargs(callback, tuple(available_bounds))
|
|
303
|
+
bound = functools.partial(
|
|
304
|
+
callback, **{k: v for k, v in available_bounds.items() if k in requested_bounds}
|
|
305
|
+
)
|
|
306
|
+
new_callback = cast("Callable[[Unpack[Ts]], None]", bound)
|
|
307
|
+
for s in signals:
|
|
308
|
+
s.connect(new_callback)
|
|
309
|
+
return callback
|
|
310
|
+
|
|
311
|
+
return cast("_ConnectModelDecorator[ViewT, MainViewT, ModelT, Unpack[Ts]]", wrapper)
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
#: environment variable naming the backend `run` started: a process the app starts again,
|
|
315
|
+
#: e.g. the web's ``--reload`` server, gets the command line without ``--backend``
|
|
316
|
+
BACKEND_VARIABLE = "COMPOSEUI_BACKEND"
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def run(
|
|
320
|
+
build_main_view: Callable[[], MainViewT],
|
|
321
|
+
*,
|
|
322
|
+
build_model: Callable[[], ModelT] | None = None,
|
|
323
|
+
setup_model: Callable[[MainViewT, ModelT], None] | None = None,
|
|
324
|
+
default_backend: RendererType = RendererType.WEB,
|
|
325
|
+
) -> None:
|
|
326
|
+
"""Run the app on the backend the command line names, e.g. ``--backend qt``.
|
|
327
|
+
|
|
328
|
+
Without ``--backend``, on the one `BACKEND_VARIABLE` names, else on `default_backend`.
|
|
329
|
+
The rest of the command line goes to the backend's app (``-f``, ``--language``,
|
|
330
|
+
``--port``, ``--help``...). Only that backend is imported, so the others' extras
|
|
331
|
+
needn't be installed; without its own, it stops and names the extra. With `build_model`,
|
|
332
|
+
the app has a model, wired to its main view by `setup_model`: once, or per session on the
|
|
333
|
+
web.
|
|
334
|
+
"""
|
|
335
|
+
if build_model is None and setup_model is not None:
|
|
336
|
+
msg = "setup_model needs a model: pass build_model too"
|
|
337
|
+
raise ValueError(msg)
|
|
338
|
+
backends = {backend.name.lower(): backend for backend in RendererType}
|
|
339
|
+
parser = argparse.ArgumentParser(add_help=False)
|
|
340
|
+
default = os.environ.get(BACKEND_VARIABLE, "")
|
|
341
|
+
if default not in backends:
|
|
342
|
+
default = default_backend.name.lower()
|
|
343
|
+
parser.add_argument("--backend", choices=backends, default=default)
|
|
344
|
+
args, rest = parser.parse_known_args()
|
|
345
|
+
# the app parses the command line itself when it runs; a process it starts again
|
|
346
|
+
# inherits its environment, and so the backend
|
|
347
|
+
sys.argv = [sys.argv[0], *rest]
|
|
348
|
+
os.environ[BACKEND_VARIABLE] = args.backend
|
|
349
|
+
backend = backends[args.backend].value
|
|
350
|
+
try:
|
|
351
|
+
module = importlib.import_module(backend.applications)
|
|
352
|
+
except ImportError:
|
|
353
|
+
# only the extra not installed: another import error is a bug, shown as it is
|
|
354
|
+
missing = [name for name in backend.packages if importlib.util.find_spec(name) is None]
|
|
355
|
+
if not missing:
|
|
356
|
+
raise
|
|
357
|
+
parser.error(
|
|
358
|
+
f"the {args.backend} backend needs {', '.join(missing)}, from the "
|
|
359
|
+
f'{backend.extra} extra: pip install "composeui[{backend.extra}]"'
|
|
360
|
+
)
|
|
361
|
+
app: Application[MainViewT]
|
|
362
|
+
if build_model is None:
|
|
363
|
+
app = module.APPLICATION(build_main_view=build_main_view)
|
|
364
|
+
else:
|
|
365
|
+
app = module.MODEL_APPLICATION(
|
|
366
|
+
build_main_view=build_main_view, build_model=build_model, setup_model=setup_model
|
|
367
|
+
)
|
|
368
|
+
app.run()
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""The series colors of the backends drawing charts without a palette of their own."""
|
|
2
|
+
|
|
3
|
+
#: categorical hues, in order, told apart with color blindness too, for a light and for a
|
|
4
|
+
#: dark background (each stepped for its background); past them, around again dashed
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
__all__ = []
|
|
8
|
+
|
|
9
|
+
SERIES_COLORS = {
|
|
10
|
+
False: ("#2a78d6", "#eb6834", "#1baf7a", "#eda100", "#e87ba4", "#008300", "#6250d6", "#e34948"),
|
|
11
|
+
True: ("#3987e5", "#d95926", "#199e70", "#c98500", "#d55181", "#008300", "#9085e9", "#e66767"),
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def series_color(index: int, *, is_dark: bool) -> str:
|
|
16
|
+
"""Get the color of the chart's series at `index`, as ``#rrggbb``."""
|
|
17
|
+
colors = SERIES_COLORS[is_dark]
|
|
18
|
+
return colors[index % len(colors)]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def is_dashed(index: int) -> bool:
|
|
22
|
+
"""Tell whether the series at `index` is dashed: the colors went around once already."""
|
|
23
|
+
return index >= len(SERIES_COLORS[False])
|