pysual 0.1.2__py3-none-win_amd64.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.
- pysual/__init__.py +154 -0
- pysual/__main__.py +6 -0
- pysual/_blueprint.py +102 -0
- pysual/_build_assets.py +53 -0
- pysual/_catalog.py +91 -0
- pysual/_config.py +236 -0
- pysual/_controls/__init__.py +5 -0
- pysual/_controls/actions.py +164 -0
- pysual/_controls/basic.py +150 -0
- pysual/_controls/choices.py +253 -0
- pysual/_controls/indicators.py +328 -0
- pysual/_controls/lists.py +261 -0
- pysual/_controls/media.py +39 -0
- pysual/_controls/ranges.py +357 -0
- pysual/_controls/viewports.py +148 -0
- pysual/_controls/windows.py +324 -0
- pysual/_drawing.py +70 -0
- pysual/_engine.py +718 -0
- pysual/_factory_types.py +8 -0
- pysual/_factory_types.pyi +1137 -0
- pysual/_grapheme_data.py +2025 -0
- pysual/_image_fit.py +22 -0
- pysual/_modality.py +327 -0
- pysual/_namespace_handlers.py +105 -0
- pysual/_nine_slice.py +41 -0
- pysual/_png.py +339 -0
- pysual/_ranges.py +35 -0
- pysual/_registry.py +173 -0
- pysual/_scrolling.py +140 -0
- pysual/_terminal_data.py +124 -0
- pysual/_text_display.py +6 -0
- pysual/_value_bag.py +20 -0
- pysual/_value_bag.pyi +69 -0
- pysual/app.py +441 -0
- pysual/assets/DejaVu-LICENSE.txt +187 -0
- pysual/assets/DejaVuSans.ttf +0 -0
- pysual/assets/DejaVuSansMono.ttf +0 -0
- pysual/assets/UNICODE-LICENSE.txt +39 -0
- pysual/assets/manifest.json +12 -0
- pysual/autoconfig.py +11 -0
- pysual/backends/__init__.py +140 -0
- pysual/backends/_files.py +190 -0
- pysual/backends/_font.py +182 -0
- pysual/backends/_gradient.py +29 -0
- pysual/backends/_native.py +105 -0
- pysual/backends/_native_client.py +592 -0
- pysual/backends/_term_cells.py +1007 -0
- pysual/backends/_term_images.py +278 -0
- pysual/backends/_term_input.py +299 -0
- pysual/backends/_term_io.py +284 -0
- pysual/backends/_term_session.py +516 -0
- pysual/backends/_web_native.py +949 -0
- pysual/backends/_web_pyodide.py +410 -0
- pysual/backends/_web_svg.py +957 -0
- pysual/backends/native.py +714 -0
- pysual/backends/term_text.py +744 -0
- pysual/backends/terminal.py +358 -0
- pysual/backends/web.py +32 -0
- pysual/behaviors.py +94 -0
- pysual/bin/SDL3-3.4.16-LICENSE.txt +18 -0
- pysual/bin/SDL3.dll +0 -0
- pysual/bin/SDL3_image-3.4.6-LICENSE.txt +17 -0
- pysual/bin/SDL3_image.dll +0 -0
- pysual/bin/SDL3_ttf-3.2.2-LICENSE.txt +17 -0
- pysual/bin/SDL3_ttf-FreeType-BDF-LICENSE.txt +152 -0
- pysual/bin/SDL3_ttf-FreeType-FTL.txt +169 -0
- pysual/bin/SDL3_ttf-FreeType-LICENSE.txt +46 -0
- pysual/bin/SDL3_ttf-FreeType-PCF-LICENSE.txt +96 -0
- pysual/bin/SDL3_ttf-HarfBuzz-LICENSE.txt +42 -0
- pysual/bin/SDL3_ttf-NOTICES.txt +50 -0
- pysual/bin/SDL3_ttf-PlutoSVG-LICENSE.txt +21 -0
- pysual/bin/SDL3_ttf-PlutoVG-LICENSE.txt +21 -0
- pysual/bin/SDL3_ttf.dll +0 -0
- pysual/bin/cJSON-LICENSE.txt +19 -0
- pysual/bin/native-build.json +24 -0
- pysual/bin/pysual-host.exe +0 -0
- pysual/bundle.py +210 -0
- pysual/cards.py +458 -0
- pysual/charts.py +441 -0
- pysual/cli.py +296 -0
- pysual/commands.py +123 -0
- pysual/composition.py +426 -0
- pysual/controls.py +1348 -0
- pysual/data_grid.py +1173 -0
- pysual/dates.py +359 -0
- pysual/editing.py +178 -0
- pysual/editors.py +372 -0
- pysual/errors.py +25 -0
- pysual/events.py +489 -0
- pysual/extras.py +113 -0
- pysual/feedback.py +399 -0
- pysual/fields.py +221 -0
- pysual/geometry.py +40 -0
- pysual/graphemes.py +141 -0
- pysual/hints.py +129 -0
- pysual/host.py +535 -0
- pysual/icons.py +308 -0
- pysual/image_resources.py +57 -0
- pysual/input.py +941 -0
- pysual/inspection.py +120 -0
- pysual/layout.py +875 -0
- pysual/menus.py +682 -0
- pysual/motion.py +273 -0
- pysual/navigation.py +458 -0
- pysual/painting.py +1562 -0
- pysual/popup.py +140 -0
- pysual/presentation.py +81 -0
- pysual/py.typed +0 -0
- pysual/render_cache.py +337 -0
- pysual/runtime.py +1112 -0
- pysual/schema.py +730 -0
- pysual/selection.py +15 -0
- pysual/text.py +1293 -0
- pysual/theme.py +1720 -0
- pysual/tree.py +534 -0
- pysual/web/__init__.py +1 -0
- pysual/web/host.js +329 -0
- pysual/web/input.js +52 -0
- pysual/web/live.html +31 -0
- pysual/web/live.js +195 -0
- pysual/web/notices/CPython-LICENSE.txt +1134 -0
- pysual/web/notices/Emscripten-LICENSE.txt +102 -0
- pysual/web/notices/HACL-LICENSE.txt +23 -0
- pysual/web/notices/LLVM-RUNTIME-LICENSES.txt +1266 -0
- pysual/web/notices/MiniLZ4-LICENSE.txt +29 -0
- pysual/web/notices/Pyodide-LICENSE.txt +373 -0
- pysual/web/notices/WEB-NOTICES.txt +42 -0
- pysual/web/notices/bzip2-LICENSE.txt +42 -0
- pysual/web/notices/hiwire-LICENSE.txt +373 -0
- pysual/web/notices/libffi-LICENSE.txt +21 -0
- pysual/web/notices/manifest.json +429 -0
- pysual/web/notices/musl-COPYRIGHT.txt +193 -0
- pysual/web/notices/musl-INCORPORATED-NOTICES.txt +1000 -0
- pysual/web/notices/zlib-LICENSE.txt +22 -0
- pysual/web/services.js +217 -0
- pysual/web/standalone.js +91 -0
- pysual/web/svg.js +254 -0
- pysual/widgets.py +22 -0
- pysual-0.1.2.dist-info/METADATA +236 -0
- pysual-0.1.2.dist-info/RECORD +144 -0
- pysual-0.1.2.dist-info/WHEEL +5 -0
- pysual-0.1.2.dist-info/entry_points.txt +2 -0
- pysual-0.1.2.dist-info/licenses/LICENSE +21 -0
- pysual-0.1.2.dist-info/top_level.txt +1 -0
pysual/__init__.py
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""Pysual: typed Python controls with shared desktop and web behavior."""
|
|
2
|
+
|
|
3
|
+
from .app import App as App, Window as Window
|
|
4
|
+
from ._config import configure as configure
|
|
5
|
+
from ._engine import shutdown as shutdown
|
|
6
|
+
from .runtime import wait as wait, wait_async as wait_async
|
|
7
|
+
from .backends.terminal import terminal as terminal
|
|
8
|
+
|
|
9
|
+
__version__ = "0.1.2"
|
|
10
|
+
from .controls import (
|
|
11
|
+
Button as Button,
|
|
12
|
+
Container as Container,
|
|
13
|
+
Control as Control,
|
|
14
|
+
Label as Label,
|
|
15
|
+
)
|
|
16
|
+
from .controls import register_control as register_control
|
|
17
|
+
from .controls import registered_controls as registered_controls
|
|
18
|
+
from .events import (
|
|
19
|
+
ClickEvent as ClickEvent,
|
|
20
|
+
ClosingEvent as ClosingEvent,
|
|
21
|
+
Event as Event,
|
|
22
|
+
UiEvent as UiEvent,
|
|
23
|
+
)
|
|
24
|
+
from .schema import Dirty as Dirty, prop as prop
|
|
25
|
+
from .behaviors import PressBehavior as PressBehavior
|
|
26
|
+
|
|
27
|
+
from .widgets import (
|
|
28
|
+
CheckBox as CheckBox,
|
|
29
|
+
Slider as Slider,
|
|
30
|
+
TextBox as TextBox,
|
|
31
|
+
ScrollArea as ScrollArea,
|
|
32
|
+
ListView as ListView,
|
|
33
|
+
Image as Image,
|
|
34
|
+
SubWindow as SubWindow,
|
|
35
|
+
)
|
|
36
|
+
from .events import (
|
|
37
|
+
ChangeEvent as ChangeEvent,
|
|
38
|
+
PointerEvent as PointerEvent,
|
|
39
|
+
KeyEvent as KeyEvent,
|
|
40
|
+
)
|
|
41
|
+
from .theme import (
|
|
42
|
+
Theme as Theme,
|
|
43
|
+
Tokens as Tokens,
|
|
44
|
+
Style as Style,
|
|
45
|
+
Rule as Rule,
|
|
46
|
+
dark as dark,
|
|
47
|
+
get_theme as get_theme,
|
|
48
|
+
theme_names as theme_names,
|
|
49
|
+
light as light,
|
|
50
|
+
load_theme as load_theme,
|
|
51
|
+
save_theme as save_theme,
|
|
52
|
+
)
|
|
53
|
+
from .geometry import Rect as Rect
|
|
54
|
+
from .painting import Painter as Painter
|
|
55
|
+
|
|
56
|
+
from .host import CapabilityError as CapabilityError
|
|
57
|
+
from .host import TextFile as TextFile
|
|
58
|
+
from .host import RenderDiagnostics as RenderDiagnostics
|
|
59
|
+
from .runtime import FrameTiming as FrameTiming
|
|
60
|
+
from .tree import TreeNode as TreeNode, TreeView as TreeView, TreeEvent as TreeEvent
|
|
61
|
+
from .popup import Popup as Popup
|
|
62
|
+
from .charts import LineChart as LineChart, ChartSeries as ChartSeries
|
|
63
|
+
from .data_grid import (
|
|
64
|
+
DataGrid as DataGrid,
|
|
65
|
+
GridColumn as GridColumn,
|
|
66
|
+
GridRow as GridRow,
|
|
67
|
+
CellValue as CellValue,
|
|
68
|
+
GridCellEvent as GridCellEvent,
|
|
69
|
+
GridEditEvent as GridEditEvent,
|
|
70
|
+
)
|
|
71
|
+
from .commands import (
|
|
72
|
+
MenuItem as MenuItem,
|
|
73
|
+
MenuGroup as MenuGroup,
|
|
74
|
+
CommandEvent as CommandEvent,
|
|
75
|
+
)
|
|
76
|
+
from .menus import Menu as Menu, MenuBar as MenuBar
|
|
77
|
+
from .composition import (
|
|
78
|
+
TabControl as TabControl,
|
|
79
|
+
TabPage as TabPage,
|
|
80
|
+
SplitPane as SplitPane,
|
|
81
|
+
)
|
|
82
|
+
from .selection import (
|
|
83
|
+
Dropdown as Dropdown,
|
|
84
|
+
ComboBox as ComboBox,
|
|
85
|
+
Toggle as Toggle,
|
|
86
|
+
RadioButton as RadioButton,
|
|
87
|
+
ProgressBar as ProgressBar,
|
|
88
|
+
Rating as Rating,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
from .icons import icon_names as icon_names
|
|
92
|
+
|
|
93
|
+
from .render_cache import CacheStats as CacheStats
|
|
94
|
+
|
|
95
|
+
from .editors import NumericInput as NumericInput, ColorPicker as ColorPicker
|
|
96
|
+
|
|
97
|
+
from .extras import GroupBox as GroupBox, Separator as Separator, Hyperlink as Hyperlink
|
|
98
|
+
from .charts import DonutChart as DonutChart, ChartSlice as ChartSlice
|
|
99
|
+
from .events import DragPayload as DragPayload, DragEvent as DragEvent
|
|
100
|
+
from .host import Viewport as Viewport
|
|
101
|
+
from .inspection import (
|
|
102
|
+
ControlSnapshot as ControlSnapshot,
|
|
103
|
+
HandlerSnapshot as HandlerSnapshot,
|
|
104
|
+
TaskSnapshot as TaskSnapshot,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
from .controls import blueprint as blueprint
|
|
108
|
+
from ._blueprint import Blueprint as Blueprint
|
|
109
|
+
|
|
110
|
+
from .image_resources import image_source as image_source, package_image as package_image
|
|
111
|
+
from .editing import EditorContent as EditorContent, EditorPopup as EditorPopup
|
|
112
|
+
from .editors import NumericEditor as NumericEditor, ColorEditor as ColorEditor
|
|
113
|
+
from .fields import SearchField as SearchField
|
|
114
|
+
from .dates import DatePicker as DatePicker, CalendarEditor as CalendarEditor
|
|
115
|
+
from ._controls.indicators import (
|
|
116
|
+
RangeSlider as RangeSlider, CircularProgress as CircularProgress,
|
|
117
|
+
)
|
|
118
|
+
from .navigation import (
|
|
119
|
+
SegmentedControl as SegmentedControl, Breadcrumb as Breadcrumb,
|
|
120
|
+
BreadcrumbEvent as BreadcrumbEvent, Pagination as Pagination,
|
|
121
|
+
)
|
|
122
|
+
from .cards import ChoiceCard as ChoiceCard, Disclosure as Disclosure
|
|
123
|
+
from .feedback import Badge as Badge, AlertBanner as AlertBanner, Meter as Meter
|
|
124
|
+
|
|
125
|
+
from ._catalog import install_builtin_controls as _install_builtin_controls
|
|
126
|
+
|
|
127
|
+
_install_builtin_controls()
|
|
128
|
+
|
|
129
|
+
# Keep loaded implementation modules out of wildcard imports. Argument
|
|
130
|
+
# processing remains an explicit opt-in through pysual.autoconfig.
|
|
131
|
+
__all__ = (
|
|
132
|
+
"App", "Window", "configure", "shutdown", "wait", "wait_async", "terminal",
|
|
133
|
+
"Control", "Container", "Button", "Label", "register_control",
|
|
134
|
+
"registered_controls", "blueprint", "Blueprint", "Dirty", "prop", "PressBehavior",
|
|
135
|
+
"CheckBox", "Slider", "TextBox", "ScrollArea", "ListView", "Image",
|
|
136
|
+
"SubWindow", "Popup", "TreeNode", "TreeView", "TreeEvent",
|
|
137
|
+
"MenuItem", "MenuGroup", "Menu", "MenuBar", "TabControl", "TabPage",
|
|
138
|
+
"SplitPane", "Dropdown", "ComboBox", "Toggle", "RadioButton", "ProgressBar", "Rating",
|
|
139
|
+
"NumericInput", "ColorPicker", "GroupBox", "Separator", "Hyperlink",
|
|
140
|
+
"EditorContent", "EditorPopup", "NumericEditor", "ColorEditor",
|
|
141
|
+
"SearchField", "DatePicker", "CalendarEditor",
|
|
142
|
+
"RangeSlider", "CircularProgress", "SegmentedControl", "Breadcrumb",
|
|
143
|
+
"BreadcrumbEvent", "Pagination",
|
|
144
|
+
"ChoiceCard", "Disclosure", "Badge", "AlertBanner", "Meter",
|
|
145
|
+
"LineChart", "ChartSeries", "DonutChart", "ChartSlice", "DataGrid",
|
|
146
|
+
"GridColumn", "GridRow", "CellValue", "GridCellEvent", "GridEditEvent",
|
|
147
|
+
"Event", "UiEvent", "ClickEvent", "ClosingEvent", "ChangeEvent",
|
|
148
|
+
"PointerEvent", "KeyEvent", "CommandEvent", "DragPayload", "DragEvent",
|
|
149
|
+
"Theme", "Tokens", "Style", "Rule", "dark", "light", "get_theme",
|
|
150
|
+
"theme_names", "load_theme", "save_theme", "icon_names",
|
|
151
|
+
"Rect", "Painter", "CapabilityError", "TextFile", "Viewport",
|
|
152
|
+
"FrameTiming", "RenderDiagnostics", "CacheStats", "ControlSnapshot", "HandlerSnapshot",
|
|
153
|
+
"TaskSnapshot", "image_source", "package_image",
|
|
154
|
+
)
|
pysual/__main__.py
ADDED
pysual/_blueprint.py
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""Immutable construction descriptors; live controls remain in their ordinary tree."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field, replace
|
|
6
|
+
import keyword
|
|
7
|
+
from types import MappingProxyType
|
|
8
|
+
from typing import TYPE_CHECKING, Any, Generic, Mapping, TypeVar, overload
|
|
9
|
+
|
|
10
|
+
from .errors import BindingError, LifecycleError
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from .controls import Container, Control
|
|
14
|
+
|
|
15
|
+
C = TypeVar("C", bound="Control")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True, eq=False)
|
|
19
|
+
class Blueprint(Generic[C]):
|
|
20
|
+
"""A typed per-instance declaration, created with pysual.blueprint()."""
|
|
21
|
+
|
|
22
|
+
control_type: type[C]
|
|
23
|
+
_properties: tuple[tuple[str, object], ...]
|
|
24
|
+
parent_name: str | None = None
|
|
25
|
+
_name: str | None = field(default=None, init=False, repr=False)
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def properties(self) -> Mapping[str, object]:
|
|
29
|
+
return MappingProxyType(dict(self._properties))
|
|
30
|
+
|
|
31
|
+
def under(self, parent_name: str) -> Blueprint[C]:
|
|
32
|
+
"""Use a Container declaration in the same owner as the visual parent."""
|
|
33
|
+
if (
|
|
34
|
+
not isinstance(parent_name, str)
|
|
35
|
+
or not parent_name.isidentifier()
|
|
36
|
+
or parent_name.startswith("_")
|
|
37
|
+
or keyword.iskeyword(parent_name)
|
|
38
|
+
):
|
|
39
|
+
raise BindingError("Blueprint parent must be a public declaration name")
|
|
40
|
+
return replace(self, parent_name=parent_name)
|
|
41
|
+
|
|
42
|
+
def __set_name__(self, owner: type, name: str) -> None:
|
|
43
|
+
if self._name is not None:
|
|
44
|
+
raise BindingError("Create a fresh blueprint for each class declaration")
|
|
45
|
+
if (
|
|
46
|
+
not name.isidentifier()
|
|
47
|
+
or name.startswith("_")
|
|
48
|
+
or keyword.iskeyword(name)
|
|
49
|
+
or name == owner.__name__
|
|
50
|
+
or any(
|
|
51
|
+
name in vars(base) and not isinstance(vars(base)[name], Blueprint)
|
|
52
|
+
for base in owner.__mro__[1:]
|
|
53
|
+
)
|
|
54
|
+
):
|
|
55
|
+
raise BindingError(f"Blueprint {name!r} collides with a framework member")
|
|
56
|
+
object.__setattr__(self, "_name", name)
|
|
57
|
+
|
|
58
|
+
@overload
|
|
59
|
+
def __get__(self, instance: None, owner: type | None = None) -> Blueprint[C]: ...
|
|
60
|
+
|
|
61
|
+
@overload
|
|
62
|
+
def __get__(self, instance: Container, owner: type | None = None) -> C: ...
|
|
63
|
+
|
|
64
|
+
def __get__(
|
|
65
|
+
self, instance: Container | None, owner: type | None = None
|
|
66
|
+
) -> Blueprint[C] | C:
|
|
67
|
+
if instance is None:
|
|
68
|
+
return self
|
|
69
|
+
from ._engine import call, is_ui_thread
|
|
70
|
+
|
|
71
|
+
if not is_ui_thread():
|
|
72
|
+
return call(self.__get__, instance, owner)
|
|
73
|
+
instance._check_live()
|
|
74
|
+
assert self._name is not None
|
|
75
|
+
value = vars(instance).get(self._name)
|
|
76
|
+
if not isinstance(value, self.control_type):
|
|
77
|
+
raise LifecycleError(
|
|
78
|
+
f"Blueprint {self._name!r} is available after construction completes"
|
|
79
|
+
)
|
|
80
|
+
return value
|
|
81
|
+
|
|
82
|
+
def __set__(self, instance: Container, value: C) -> None:
|
|
83
|
+
if not isinstance(value, self.control_type):
|
|
84
|
+
raise TypeError(
|
|
85
|
+
f"Blueprint {self._name!r} requires {self.control_type.__name__}"
|
|
86
|
+
)
|
|
87
|
+
assert self._name is not None
|
|
88
|
+
vars(instance)[self._name] = value
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def declarations(owner_type: type) -> dict[str, Blueprint[Any]]:
|
|
92
|
+
"""Resolve inherited declarations without evaluating descriptors or user code."""
|
|
93
|
+
result: dict[str, Blueprint[Any]] = {}
|
|
94
|
+
for base in reversed(owner_type.__mro__):
|
|
95
|
+
for name, value in vars(base).items():
|
|
96
|
+
if isinstance(value, Blueprint):
|
|
97
|
+
result[name] = value
|
|
98
|
+
elif name in result:
|
|
99
|
+
raise BindingError(
|
|
100
|
+
f"Override inherited blueprint {name!r} with a blueprint"
|
|
101
|
+
)
|
|
102
|
+
return result
|
pysual/_build_assets.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Validate application assets without traversing filesystem redirects."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
import os
|
|
5
|
+
import stat
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _checked_path(root: Path, path: Path) -> tuple[os.stat_result, Path]:
|
|
9
|
+
# Check every component: an explicitly included file can itself have a
|
|
10
|
+
# junction/symlink parent. st_file_attributes works on Python 3.11 too.
|
|
11
|
+
current = root
|
|
12
|
+
for part in path.relative_to(root).parts:
|
|
13
|
+
current /= part
|
|
14
|
+
info = current.lstat()
|
|
15
|
+
if stat.S_ISLNK(info.st_mode) or getattr(info, "st_file_attributes", 0) & 0x400:
|
|
16
|
+
raise ValueError(f"Included symlinks and reparse points are unsupported: {current}")
|
|
17
|
+
resolved = path.resolve(strict=True)
|
|
18
|
+
if not resolved.is_relative_to(root):
|
|
19
|
+
raise ValueError(f"Included files must be inside the application directory: {path}")
|
|
20
|
+
return path.lstat(), resolved
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def include_files(root: Path, item: str | Path, output: Path) -> tuple[Path, list[Path]]:
|
|
24
|
+
"""Return a validated include root and its regular files, in stable order.
|
|
25
|
+
|
|
26
|
+
Inputs must remain unchanged while a build runs. This is a packaging
|
|
27
|
+
boundary, not a sandbox against concurrent hostile filesystem mutation.
|
|
28
|
+
"""
|
|
29
|
+
relative = Path(item)
|
|
30
|
+
if relative.is_absolute() or ".." in relative.parts:
|
|
31
|
+
raise ValueError("Included files must be inside the application directory, using relative paths")
|
|
32
|
+
candidate = root / relative
|
|
33
|
+
try:
|
|
34
|
+
info, resolved = _checked_path(root, candidate)
|
|
35
|
+
# Windows accepts aliases such as a trailing dot or an 8.3 short name.
|
|
36
|
+
# Compare canonical paths only after checking lexical redirect parents.
|
|
37
|
+
if output == resolved or stat.S_ISDIR(info.st_mode) and output.is_relative_to(resolved):
|
|
38
|
+
raise ValueError("The generated output cannot be inside an included path")
|
|
39
|
+
pending: list[Path] = [candidate]
|
|
40
|
+
files: list[Path] = []
|
|
41
|
+
while pending:
|
|
42
|
+
path = pending.pop()
|
|
43
|
+
info, _ = _checked_path(root, path)
|
|
44
|
+
if stat.S_ISDIR(info.st_mode):
|
|
45
|
+
# Validate before descending, so junction cycles cannot recurse.
|
|
46
|
+
pending.extend(reversed(sorted(path.iterdir())))
|
|
47
|
+
elif stat.S_ISREG(info.st_mode):
|
|
48
|
+
files.append(path)
|
|
49
|
+
else:
|
|
50
|
+
raise ValueError(f"Included path must be a regular file or directory: {path}")
|
|
51
|
+
except OSError as error:
|
|
52
|
+
raise ValueError(f"Cannot read included path {item}: {error}") from error
|
|
53
|
+
return candidate, files
|
pysual/_catalog.py
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""Built-in catalog metadata and installation, independent of implementation files.
|
|
2
|
+
|
|
3
|
+
New entries default to constructor/create-only registration. The historical
|
|
4
|
+
factory aliases are explicit compatibility choices, not an ever-growing set of
|
|
5
|
+
reserved Container members. Public Python exports remain explicit for typing.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
|
|
10
|
+
from .controls import Button, Container, Control, Label
|
|
11
|
+
from .widgets import CheckBox, Image, ListView, ScrollArea, Slider, SubWindow, TextBox
|
|
12
|
+
from .selection import ComboBox, Dropdown, ProgressBar, RadioButton, Rating, Toggle
|
|
13
|
+
from .composition import SplitPane, TabControl, TabPage
|
|
14
|
+
from .charts import DonutChart, LineChart
|
|
15
|
+
from .data_grid import DataGrid
|
|
16
|
+
from .editors import ColorPicker, NumericInput
|
|
17
|
+
from .fields import SearchField
|
|
18
|
+
from .dates import DatePicker
|
|
19
|
+
from ._controls.indicators import RangeSlider, CircularProgress
|
|
20
|
+
from .navigation import SegmentedControl, Breadcrumb, Pagination
|
|
21
|
+
from .cards import ChoiceCard, Disclosure
|
|
22
|
+
from .feedback import Badge, AlertBanner, Meter
|
|
23
|
+
from .extras import GroupBox, Hyperlink, Separator
|
|
24
|
+
from .menus import Menu, MenuBar
|
|
25
|
+
from .popup import Popup
|
|
26
|
+
from .tree import TreeView
|
|
27
|
+
from ._registry import register_control
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True)
|
|
31
|
+
class BuiltinControl:
|
|
32
|
+
name: str
|
|
33
|
+
control_type: type[Control]
|
|
34
|
+
family: str
|
|
35
|
+
legacy_factory: bool = False
|
|
36
|
+
authorable: bool = True
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
BUILTIN_CONTROLS = (
|
|
40
|
+
BuiltinControl("label", Label, "basic", legacy_factory=True),
|
|
41
|
+
BuiltinControl("button", Button, "basic", legacy_factory=True),
|
|
42
|
+
BuiltinControl("panel", Container, "layout", legacy_factory=True),
|
|
43
|
+
BuiltinControl("check_box", CheckBox, "actions", legacy_factory=True),
|
|
44
|
+
BuiltinControl("slider", Slider, "ranges", legacy_factory=True),
|
|
45
|
+
BuiltinControl("text_box", TextBox, "text", legacy_factory=True),
|
|
46
|
+
BuiltinControl("scroll_area", ScrollArea, "viewports", legacy_factory=True),
|
|
47
|
+
BuiltinControl("list_view", ListView, "collections", legacy_factory=True),
|
|
48
|
+
BuiltinControl("image", Image, "media", legacy_factory=True),
|
|
49
|
+
BuiltinControl("sub_window", SubWindow, "windows", legacy_factory=True),
|
|
50
|
+
BuiltinControl("tree_view", TreeView, "collections", legacy_factory=True),
|
|
51
|
+
BuiltinControl("popup", Popup, "overlays", legacy_factory=True),
|
|
52
|
+
BuiltinControl("line_chart", LineChart, "charts", legacy_factory=True),
|
|
53
|
+
BuiltinControl("donut_chart", DonutChart, "charts", legacy_factory=True),
|
|
54
|
+
BuiltinControl("data_grid", DataGrid, "collections", legacy_factory=True),
|
|
55
|
+
BuiltinControl("menu", Menu, "commands", legacy_factory=True),
|
|
56
|
+
BuiltinControl("menu_bar", MenuBar, "commands", legacy_factory=True),
|
|
57
|
+
BuiltinControl("tab_page", TabPage, "layout", legacy_factory=True),
|
|
58
|
+
BuiltinControl("tab_control", TabControl, "layout", legacy_factory=True),
|
|
59
|
+
BuiltinControl("split_pane", SplitPane, "layout", legacy_factory=True),
|
|
60
|
+
BuiltinControl("toggle", Toggle, "actions", legacy_factory=True),
|
|
61
|
+
BuiltinControl("radio_button", RadioButton, "actions", legacy_factory=True),
|
|
62
|
+
BuiltinControl("progress_bar", ProgressBar, "ranges", legacy_factory=True),
|
|
63
|
+
BuiltinControl("dropdown", Dropdown, "choices", legacy_factory=True),
|
|
64
|
+
BuiltinControl("combo_box", ComboBox, "choices", legacy_factory=True),
|
|
65
|
+
BuiltinControl("numeric_input", NumericInput, "editors", legacy_factory=True),
|
|
66
|
+
BuiltinControl("color_picker", ColorPicker, "editors", legacy_factory=True),
|
|
67
|
+
BuiltinControl("group_box", GroupBox, "layout", legacy_factory=True),
|
|
68
|
+
BuiltinControl("separator", Separator, "layout", legacy_factory=True),
|
|
69
|
+
BuiltinControl("hyperlink", Hyperlink, "actions", legacy_factory=True),
|
|
70
|
+
BuiltinControl("rating", Rating, "ranges"),
|
|
71
|
+
BuiltinControl("search_field", SearchField, "text"),
|
|
72
|
+
BuiltinControl("date_picker", DatePicker, "dates"),
|
|
73
|
+
BuiltinControl("range_slider", RangeSlider, "ranges"),
|
|
74
|
+
BuiltinControl("circular_progress", CircularProgress, "ranges"),
|
|
75
|
+
BuiltinControl("segmented_control", SegmentedControl, "actions"),
|
|
76
|
+
BuiltinControl("breadcrumb", Breadcrumb, "navigation"),
|
|
77
|
+
BuiltinControl("pagination", Pagination, "navigation"),
|
|
78
|
+
BuiltinControl("choice_card", ChoiceCard, "actions"),
|
|
79
|
+
BuiltinControl("disclosure", Disclosure, "layout"),
|
|
80
|
+
BuiltinControl("badge", Badge, "feedback"),
|
|
81
|
+
BuiltinControl("alert_banner", AlertBanner, "feedback"),
|
|
82
|
+
BuiltinControl("meter", Meter, "feedback"),
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def install_builtin_controls() -> None:
|
|
87
|
+
for entry in BUILTIN_CONTROLS:
|
|
88
|
+
register_control(
|
|
89
|
+
entry.control_type, name=entry.name, authorable=entry.authorable,
|
|
90
|
+
factory=entry.legacy_factory,
|
|
91
|
+
)
|
pysual/_config.py
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
"""Validated startup defaults; external configuration is explicitly opt-in."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
import math
|
|
7
|
+
import os
|
|
8
|
+
import sys
|
|
9
|
+
from threading import RLock
|
|
10
|
+
from typing import TYPE_CHECKING, cast
|
|
11
|
+
|
|
12
|
+
from .backends import BACKEND_NAMES, BackendName
|
|
13
|
+
from .errors import LifecycleError
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from .host import Host
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True)
|
|
20
|
+
class Settings:
|
|
21
|
+
backend: str | None = None
|
|
22
|
+
terminal_renderer: str | None = None
|
|
23
|
+
theme: str | None = None
|
|
24
|
+
ui_scale: float | None = None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(frozen=True)
|
|
28
|
+
class Option:
|
|
29
|
+
field: str
|
|
30
|
+
flag: str
|
|
31
|
+
environment: str
|
|
32
|
+
description: str
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
OPTIONS = (
|
|
36
|
+
Option(
|
|
37
|
+
"backend",
|
|
38
|
+
"--backend",
|
|
39
|
+
"PYSUAL_BACKEND",
|
|
40
|
+
"UI backend: " + ", ".join(BACKEND_NAMES),
|
|
41
|
+
),
|
|
42
|
+
Option(
|
|
43
|
+
"terminal_renderer",
|
|
44
|
+
"--terminal",
|
|
45
|
+
"PYSUAL_TERMINAL",
|
|
46
|
+
"Terminal renderer: auto, c, or python",
|
|
47
|
+
),
|
|
48
|
+
Option(
|
|
49
|
+
"theme",
|
|
50
|
+
"--pysual-theme",
|
|
51
|
+
"PYSUAL_THEME",
|
|
52
|
+
"Default window theme (catalog name)",
|
|
53
|
+
),
|
|
54
|
+
Option(
|
|
55
|
+
"ui_scale",
|
|
56
|
+
"--pysual-scale",
|
|
57
|
+
"PYSUAL_SCALE",
|
|
58
|
+
"Default window scale (finite number, at least 0.25)",
|
|
59
|
+
),
|
|
60
|
+
)
|
|
61
|
+
_BY_FLAG = {option.flag: option for option in OPTIONS}
|
|
62
|
+
_defaults: dict[str, object] = {}
|
|
63
|
+
_environment: dict[str, object] = {}
|
|
64
|
+
_command_line: dict[str, object] = {}
|
|
65
|
+
_activated = False
|
|
66
|
+
_frozen: Settings | None = None
|
|
67
|
+
_lock = RLock()
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _validate(name: str, value: object) -> object:
|
|
71
|
+
if name == "backend":
|
|
72
|
+
if value is not None and (
|
|
73
|
+
not isinstance(value, str) or value not in BACKEND_NAMES
|
|
74
|
+
):
|
|
75
|
+
raise ValueError("backend must be one of: " + ", ".join(BACKEND_NAMES))
|
|
76
|
+
elif name == "terminal_renderer":
|
|
77
|
+
if value is not None and value not in ("auto", "c", "python"):
|
|
78
|
+
raise ValueError("terminal renderer must be auto, c, or python")
|
|
79
|
+
elif name == "theme":
|
|
80
|
+
from .theme import theme_names
|
|
81
|
+
|
|
82
|
+
if value is not None and value not in theme_names():
|
|
83
|
+
raise ValueError("theme must be a built-in theme name")
|
|
84
|
+
elif name == "ui_scale":
|
|
85
|
+
if value is not None:
|
|
86
|
+
if type(value) is int or type(value) is float:
|
|
87
|
+
if not math.isfinite(value) or value < 0.25:
|
|
88
|
+
raise ValueError("ui_scale must be a finite number of at least 0.25")
|
|
89
|
+
else:
|
|
90
|
+
raise ValueError("ui_scale must be a finite number of at least 0.25")
|
|
91
|
+
else:
|
|
92
|
+
raise TypeError(f"Unknown startup setting {name!r}")
|
|
93
|
+
return value
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def configure(
|
|
97
|
+
*,
|
|
98
|
+
backend: str | None = None,
|
|
99
|
+
terminal_renderer: str | None = None,
|
|
100
|
+
theme: str | None = None,
|
|
101
|
+
ui_scale: float | None = None,
|
|
102
|
+
) -> Settings:
|
|
103
|
+
"""Set startup defaults before constructing UI objects; never read argv/env.
|
|
104
|
+
|
|
105
|
+
Opted-in command-line values override these defaults. Theme and scale defaults
|
|
106
|
+
do not replace an explicit window constructor argument.
|
|
107
|
+
"""
|
|
108
|
+
values = {name: value for name, value in locals().items() if value is not None}
|
|
109
|
+
values = {name: _validate(name, value) for name, value in values.items()}
|
|
110
|
+
with _lock:
|
|
111
|
+
if _frozen is not None:
|
|
112
|
+
raise LifecycleError("Configure Pysual before constructing UI objects")
|
|
113
|
+
_defaults.update(values)
|
|
114
|
+
return current()
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def current() -> Settings:
|
|
118
|
+
with _lock:
|
|
119
|
+
if _frozen is not None:
|
|
120
|
+
return _frozen
|
|
121
|
+
values = {**_environment, **_defaults, **_command_line}
|
|
122
|
+
return Settings(
|
|
123
|
+
backend=cast(str | None, values.get("backend")),
|
|
124
|
+
terminal_renderer=cast(str | None, values.get("terminal_renderer")),
|
|
125
|
+
theme=cast(str | None, values.get("theme")),
|
|
126
|
+
ui_scale=cast(float | None, values.get("ui_scale")),
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def freeze() -> Settings:
|
|
131
|
+
global _frozen
|
|
132
|
+
with _lock:
|
|
133
|
+
if _frozen is None:
|
|
134
|
+
_frozen = current()
|
|
135
|
+
return _frozen
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def window_defaults() -> dict[str, object]:
|
|
139
|
+
settings = current()
|
|
140
|
+
defaults: dict[str, object] = {}
|
|
141
|
+
if settings.ui_scale is not None:
|
|
142
|
+
defaults["ui_scale"] = settings.ui_scale
|
|
143
|
+
if settings.theme is not None:
|
|
144
|
+
from .theme import get_theme
|
|
145
|
+
|
|
146
|
+
defaults["theme"] = get_theme(settings.theme)
|
|
147
|
+
return defaults
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _convert(option: Option, value: str) -> object:
|
|
151
|
+
if option.field == "ui_scale":
|
|
152
|
+
try:
|
|
153
|
+
parsed = float(value)
|
|
154
|
+
except (ValueError, TypeError):
|
|
155
|
+
raise ValueError(f"{option.flag} requires a number") from None
|
|
156
|
+
return _validate(option.field, parsed)
|
|
157
|
+
return _validate(option.field, value)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def activate() -> None:
|
|
161
|
+
"""Consume only recognized arguments, transactionally and once per process."""
|
|
162
|
+
global _activated
|
|
163
|
+
with _lock:
|
|
164
|
+
if _activated:
|
|
165
|
+
return
|
|
166
|
+
if _frozen is not None:
|
|
167
|
+
raise LifecycleError(
|
|
168
|
+
"Import pysual.autoconfig before constructing UI objects"
|
|
169
|
+
)
|
|
170
|
+
arguments = list(sys.argv)
|
|
171
|
+
remaining = arguments[:1]
|
|
172
|
+
selected: dict[str, object] = {}
|
|
173
|
+
help_requested = False
|
|
174
|
+
index = 1
|
|
175
|
+
while index < len(arguments):
|
|
176
|
+
token = arguments[index]
|
|
177
|
+
if token == "--":
|
|
178
|
+
remaining.extend(arguments[index:])
|
|
179
|
+
break
|
|
180
|
+
flag, separator, value = token.partition("=")
|
|
181
|
+
if flag == "--pysual-help":
|
|
182
|
+
if separator:
|
|
183
|
+
raise ValueError("--pysual-help does not take a value")
|
|
184
|
+
help_requested = True
|
|
185
|
+
index += 1
|
|
186
|
+
continue
|
|
187
|
+
option = _BY_FLAG.get(flag)
|
|
188
|
+
if option is None:
|
|
189
|
+
if flag.startswith("--pysual-"):
|
|
190
|
+
raise ValueError(
|
|
191
|
+
f"Unknown Pysual option {flag!r}; use --pysual-help"
|
|
192
|
+
)
|
|
193
|
+
remaining.append(token)
|
|
194
|
+
index += 1
|
|
195
|
+
continue
|
|
196
|
+
if option.field in selected:
|
|
197
|
+
raise ValueError(f"Repeated Pysual option {flag}")
|
|
198
|
+
if not separator:
|
|
199
|
+
index += 1
|
|
200
|
+
if index >= len(arguments) or arguments[index].startswith("--"):
|
|
201
|
+
raise ValueError(f"{flag} requires a value")
|
|
202
|
+
value = arguments[index]
|
|
203
|
+
if not value:
|
|
204
|
+
raise ValueError(f"{flag} requires a value")
|
|
205
|
+
selected[option.field] = _convert(option, value)
|
|
206
|
+
index += 1
|
|
207
|
+
environment = {
|
|
208
|
+
option.field: _convert(option, os.environ[option.environment])
|
|
209
|
+
for option in OPTIONS
|
|
210
|
+
if option.environment in os.environ
|
|
211
|
+
and option.field not in selected
|
|
212
|
+
and option.field not in _defaults
|
|
213
|
+
}
|
|
214
|
+
if help_requested:
|
|
215
|
+
print("Pysual options (opt in with 'from pysual import autoconfig'):")
|
|
216
|
+
for option in OPTIONS:
|
|
217
|
+
print(f" {option.flag} VALUE {option.description}")
|
|
218
|
+
print(" --pysual-help Show this help; application --help is untouched")
|
|
219
|
+
raise SystemExit(0)
|
|
220
|
+
_environment.update(environment)
|
|
221
|
+
_command_line.update(selected)
|
|
222
|
+
sys.argv[:] = remaining
|
|
223
|
+
_activated = True
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def resolved_backend(
|
|
227
|
+
preference: Host | BackendName | None = None,
|
|
228
|
+
) -> Host | BackendName | None:
|
|
229
|
+
"""CLI overrides a named preference; concrete test Hosts remain explicit."""
|
|
230
|
+
if preference is not None and not isinstance(preference, str):
|
|
231
|
+
return preference
|
|
232
|
+
with _lock:
|
|
233
|
+
return cast(
|
|
234
|
+
BackendName | None,
|
|
235
|
+
_command_line.get("backend") or preference or current().backend,
|
|
236
|
+
)
|