trame-dataclass 1.0.0__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.
- trame_dataclass-1.0.0/.gitignore +26 -0
- trame_dataclass-1.0.0/LICENSE +15 -0
- trame_dataclass-1.0.0/PKG-INFO +100 -0
- trame_dataclass-1.0.0/README.rst +69 -0
- trame_dataclass-1.0.0/pyproject.toml +93 -0
- trame_dataclass-1.0.0/src/trame/modules/dataclass.py +1 -0
- trame_dataclass-1.0.0/src/trame/widgets/dataclass.py +7 -0
- trame_dataclass-1.0.0/src/trame_dataclass/__init__.py +1 -0
- trame_dataclass-1.0.0/src/trame_dataclass/core.py +775 -0
- trame_dataclass-1.0.0/src/trame_dataclass/module/__init__.py +29 -0
- trame_dataclass-1.0.0/src/trame_dataclass/module/protocol.py +94 -0
- trame_dataclass-1.0.0/src/trame_dataclass/module/serve/trame_dataclass.umd.js +1 -0
- trame_dataclass-1.0.0/src/trame_dataclass/widgets/__init__.py +0 -0
- trame_dataclass-1.0.0/src/trame_dataclass/widgets/dataclass.py +26 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
.DS_Store
|
|
2
|
+
node_modules
|
|
3
|
+
.venv
|
|
4
|
+
|
|
5
|
+
# local env files
|
|
6
|
+
.env.local
|
|
7
|
+
.env.*.local
|
|
8
|
+
|
|
9
|
+
# Log files
|
|
10
|
+
npm-debug.log*
|
|
11
|
+
yarn-debug.log*
|
|
12
|
+
yarn-error.log*
|
|
13
|
+
pnpm-debug.log*
|
|
14
|
+
|
|
15
|
+
# Editor directories and files
|
|
16
|
+
.idea
|
|
17
|
+
.vscode
|
|
18
|
+
*.suo
|
|
19
|
+
*.ntvs*
|
|
20
|
+
*.njsproj
|
|
21
|
+
*.sln
|
|
22
|
+
*.sw?
|
|
23
|
+
|
|
24
|
+
__pycache__
|
|
25
|
+
*egg-info
|
|
26
|
+
*pyc
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
Apache Software License 2.0
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025, Kitware Inc.
|
|
4
|
+
|
|
5
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
you may not use this file except in compliance with the License.
|
|
7
|
+
You may obtain a copy of the License at
|
|
8
|
+
|
|
9
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
|
|
11
|
+
Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
See the License for the specific language governing permissions and
|
|
15
|
+
limitations under the License.
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: trame-dataclass
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Dataclass for trame UI binding
|
|
5
|
+
Author: Kitware Inc.
|
|
6
|
+
License: Apache Software License
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Keywords: Application,Framework,Interactive,Python,Web
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Environment :: Web Environment
|
|
11
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
12
|
+
Classifier: Natural Language :: English
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
15
|
+
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
16
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
17
|
+
Requires-Python: >=3.9
|
|
18
|
+
Requires-Dist: trame-client>=3.10
|
|
19
|
+
Provides-Extra: app
|
|
20
|
+
Requires-Dist: pywebview; extra == 'app'
|
|
21
|
+
Provides-Extra: dev
|
|
22
|
+
Requires-Dist: nox; extra == 'dev'
|
|
23
|
+
Requires-Dist: pre-commit; extra == 'dev'
|
|
24
|
+
Requires-Dist: pytest-asyncio; extra == 'dev'
|
|
25
|
+
Requires-Dist: pytest-cov>=3; extra == 'dev'
|
|
26
|
+
Requires-Dist: pytest>=6; extra == 'dev'
|
|
27
|
+
Requires-Dist: ruff; extra == 'dev'
|
|
28
|
+
Provides-Extra: jupyter
|
|
29
|
+
Requires-Dist: jupyterlab; extra == 'jupyter'
|
|
30
|
+
Description-Content-Type: text/x-rst
|
|
31
|
+
|
|
32
|
+
trame-dataclass
|
|
33
|
+
----------------------------------------
|
|
34
|
+
|
|
35
|
+
Dataclass for trame UI binding
|
|
36
|
+
|
|
37
|
+
License
|
|
38
|
+
----------------------------------------
|
|
39
|
+
|
|
40
|
+
This library is OpenSource and follow the Apache Software License
|
|
41
|
+
|
|
42
|
+
Installation
|
|
43
|
+
----------------------------------------
|
|
44
|
+
|
|
45
|
+
Install the application/library
|
|
46
|
+
|
|
47
|
+
.. code-block:: console
|
|
48
|
+
|
|
49
|
+
pip install trame-dataclass
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
Development setup
|
|
53
|
+
----------------------------------------
|
|
54
|
+
|
|
55
|
+
We recommend using uv for setting up and managing a virtual environment for your development.
|
|
56
|
+
|
|
57
|
+
.. code-block:: console
|
|
58
|
+
|
|
59
|
+
# Create venv and install all dependencies
|
|
60
|
+
uv sync --all-extras --dev
|
|
61
|
+
|
|
62
|
+
# Activate environment
|
|
63
|
+
source .venv/bin/activate
|
|
64
|
+
|
|
65
|
+
# Install commit analysis
|
|
66
|
+
pre-commit install
|
|
67
|
+
pre-commit install --hook-type commit-msg
|
|
68
|
+
|
|
69
|
+
# Allow live code edit
|
|
70
|
+
uv pip install -e .
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
Build and install the Vue components
|
|
74
|
+
|
|
75
|
+
.. code-block:: console
|
|
76
|
+
|
|
77
|
+
cd vue-components
|
|
78
|
+
npm i
|
|
79
|
+
npm run build
|
|
80
|
+
cd -
|
|
81
|
+
|
|
82
|
+
For running tests and checks, you can run ``nox``.
|
|
83
|
+
|
|
84
|
+
.. code-block:: console
|
|
85
|
+
|
|
86
|
+
# run all
|
|
87
|
+
nox
|
|
88
|
+
|
|
89
|
+
# lint
|
|
90
|
+
nox -s lint
|
|
91
|
+
|
|
92
|
+
# tests
|
|
93
|
+
nox -s tests
|
|
94
|
+
|
|
95
|
+
Professional Support
|
|
96
|
+
----------------------------------------
|
|
97
|
+
|
|
98
|
+
* `Training <https://www.kitware.com/courses/trame/>`_: Learn how to confidently use trame from the expert developers at Kitware.
|
|
99
|
+
* `Support <https://www.kitware.com/trame/support/>`_: Our experts can assist your team as you build your web application and establish in-house expertise.
|
|
100
|
+
* `Custom Development <https://www.kitware.com/trame/support/>`_: Leverage Kitware’s 25+ years of experience to quickly build your web application.
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
trame-dataclass
|
|
2
|
+
----------------------------------------
|
|
3
|
+
|
|
4
|
+
Dataclass for trame UI binding
|
|
5
|
+
|
|
6
|
+
License
|
|
7
|
+
----------------------------------------
|
|
8
|
+
|
|
9
|
+
This library is OpenSource and follow the Apache Software License
|
|
10
|
+
|
|
11
|
+
Installation
|
|
12
|
+
----------------------------------------
|
|
13
|
+
|
|
14
|
+
Install the application/library
|
|
15
|
+
|
|
16
|
+
.. code-block:: console
|
|
17
|
+
|
|
18
|
+
pip install trame-dataclass
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
Development setup
|
|
22
|
+
----------------------------------------
|
|
23
|
+
|
|
24
|
+
We recommend using uv for setting up and managing a virtual environment for your development.
|
|
25
|
+
|
|
26
|
+
.. code-block:: console
|
|
27
|
+
|
|
28
|
+
# Create venv and install all dependencies
|
|
29
|
+
uv sync --all-extras --dev
|
|
30
|
+
|
|
31
|
+
# Activate environment
|
|
32
|
+
source .venv/bin/activate
|
|
33
|
+
|
|
34
|
+
# Install commit analysis
|
|
35
|
+
pre-commit install
|
|
36
|
+
pre-commit install --hook-type commit-msg
|
|
37
|
+
|
|
38
|
+
# Allow live code edit
|
|
39
|
+
uv pip install -e .
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
Build and install the Vue components
|
|
43
|
+
|
|
44
|
+
.. code-block:: console
|
|
45
|
+
|
|
46
|
+
cd vue-components
|
|
47
|
+
npm i
|
|
48
|
+
npm run build
|
|
49
|
+
cd -
|
|
50
|
+
|
|
51
|
+
For running tests and checks, you can run ``nox``.
|
|
52
|
+
|
|
53
|
+
.. code-block:: console
|
|
54
|
+
|
|
55
|
+
# run all
|
|
56
|
+
nox
|
|
57
|
+
|
|
58
|
+
# lint
|
|
59
|
+
nox -s lint
|
|
60
|
+
|
|
61
|
+
# tests
|
|
62
|
+
nox -s tests
|
|
63
|
+
|
|
64
|
+
Professional Support
|
|
65
|
+
----------------------------------------
|
|
66
|
+
|
|
67
|
+
* `Training <https://www.kitware.com/courses/trame/>`_: Learn how to confidently use trame from the expert developers at Kitware.
|
|
68
|
+
* `Support <https://www.kitware.com/trame/support/>`_: Our experts can assist your team as you build your web application and establish in-house expertise.
|
|
69
|
+
* `Custom Development <https://www.kitware.com/trame/support/>`_: Leverage Kitware’s 25+ years of experience to quickly build your web application.
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "trame-dataclass"
|
|
3
|
+
version = "1.0.0"
|
|
4
|
+
description = "Dataclass for trame UI binding"
|
|
5
|
+
authors = [{ name = "Kitware Inc." }]
|
|
6
|
+
dependencies = ["trame_client>=3.10"]
|
|
7
|
+
requires-python = ">=3.9"
|
|
8
|
+
readme = "README.rst"
|
|
9
|
+
license = { text = "Apache Software License" }
|
|
10
|
+
keywords = ["Python", "Interactive", "Web", "Application", "Framework"]
|
|
11
|
+
classifiers = [
|
|
12
|
+
"Development Status :: 4 - Beta",
|
|
13
|
+
"Environment :: Web Environment",
|
|
14
|
+
"License :: OSI Approved :: Apache Software License",
|
|
15
|
+
"Natural Language :: English",
|
|
16
|
+
"Operating System :: OS Independent",
|
|
17
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
18
|
+
"Topic :: Software Development :: Libraries :: Application Frameworks",
|
|
19
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
[project.optional-dependencies]
|
|
23
|
+
app = ["pywebview"]
|
|
24
|
+
jupyter = ["jupyterlab"]
|
|
25
|
+
dev = [
|
|
26
|
+
"pre-commit",
|
|
27
|
+
"ruff",
|
|
28
|
+
"pytest >=6",
|
|
29
|
+
"pytest-asyncio",
|
|
30
|
+
"pytest-cov >=3",
|
|
31
|
+
"nox",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
[build-system]
|
|
35
|
+
requires = ["hatchling"]
|
|
36
|
+
build-backend = "hatchling.build"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
[tool.hatch.build]
|
|
40
|
+
include = [
|
|
41
|
+
"/src/trame/**/*.py",
|
|
42
|
+
"/src/trame_dataclass/**/*.py",
|
|
43
|
+
"/src/trame_dataclass/**/*.js",
|
|
44
|
+
"/src/trame_dataclass/**/*.css",
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
[tool.hatch.build.targets.wheel]
|
|
48
|
+
packages = ["src/trame", "src/trame_dataclass"]
|
|
49
|
+
|
|
50
|
+
[tool.ruff]
|
|
51
|
+
|
|
52
|
+
[tool.ruff.lint]
|
|
53
|
+
extend-select = [
|
|
54
|
+
"ARG", # flake8-unused-arguments
|
|
55
|
+
"B", # flake8-bugbear
|
|
56
|
+
"C4", # flake8-comprehensions
|
|
57
|
+
"EM", # flake8-errmsg
|
|
58
|
+
"EXE", # flake8-executable
|
|
59
|
+
"G", # flake8-logging-format
|
|
60
|
+
"I", # isort
|
|
61
|
+
"ICN", # flake8-import-conventions
|
|
62
|
+
"NPY", # NumPy specific rules
|
|
63
|
+
"PD", # pandas-vet
|
|
64
|
+
"PGH", # pygrep-hooks
|
|
65
|
+
"PIE", # flake8-pie
|
|
66
|
+
"PL", # pylint
|
|
67
|
+
"PT", # flake8-pytest-style
|
|
68
|
+
"PTH", # flake8-use-pathlib
|
|
69
|
+
"RET", # flake8-return
|
|
70
|
+
"RUF", # Ruff-specific
|
|
71
|
+
"SIM", # flake8-simplify
|
|
72
|
+
"T20", # flake8-print
|
|
73
|
+
"UP", # pyupgrade
|
|
74
|
+
"YTT", # flake8-2020
|
|
75
|
+
]
|
|
76
|
+
ignore = [
|
|
77
|
+
"T201", # tmp during dev
|
|
78
|
+
"PLR09", # Too many <...>
|
|
79
|
+
"PLR2004", # Magic value used in comparison
|
|
80
|
+
"ISC001", # Conflicts with formatter
|
|
81
|
+
"SIM117", # nested with for UI in examples
|
|
82
|
+
]
|
|
83
|
+
isort.required-imports = []
|
|
84
|
+
|
|
85
|
+
[tool.ruff.lint.per-file-ignores]
|
|
86
|
+
"tests/**" = ["T20"]
|
|
87
|
+
"noxfile.py" = ["T20"]
|
|
88
|
+
"src/**" = ["SIM117"]
|
|
89
|
+
|
|
90
|
+
[tool.semantic_release]
|
|
91
|
+
version_toml = ["pyproject.toml:project.version"]
|
|
92
|
+
version_variables = ["src/trame_dataclass/__init__.py:__version__"]
|
|
93
|
+
build_command = "pip install uv && uv build"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from trame_dataclass.module import * # noqa: F403
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.0.0"
|
|
@@ -0,0 +1,775 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import inspect
|
|
3
|
+
import os
|
|
4
|
+
import string
|
|
5
|
+
import types
|
|
6
|
+
import warnings
|
|
7
|
+
import weakref
|
|
8
|
+
from collections.abc import Awaitable
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from enum import Enum, auto
|
|
11
|
+
from typing import Any, Callable, TypeVar
|
|
12
|
+
|
|
13
|
+
from trame_common.obj.component import TrameComponent
|
|
14
|
+
|
|
15
|
+
from trame_dataclass import module as dataclass_module
|
|
16
|
+
|
|
17
|
+
# -----------------------------------------------------------------------------
|
|
18
|
+
# internal field names
|
|
19
|
+
# -----------------------------------------------------------------------------
|
|
20
|
+
_FIELDS = "__trame_dataclass_fields__"
|
|
21
|
+
|
|
22
|
+
# -----------------------------------------------------------------------------
|
|
23
|
+
# Id generator
|
|
24
|
+
# -----------------------------------------------------------------------------
|
|
25
|
+
INSTANCES = weakref.WeakValueDictionary()
|
|
26
|
+
_INSTANCE_COUNT = 0
|
|
27
|
+
_INSTANCE_ID_CHARS = string.digits + string.ascii_letters
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _next_id():
|
|
31
|
+
global _INSTANCE_COUNT # noqa: PLW0603
|
|
32
|
+
_INSTANCE_COUNT += 1
|
|
33
|
+
|
|
34
|
+
result = []
|
|
35
|
+
value = _INSTANCE_COUNT
|
|
36
|
+
size = len(_INSTANCE_ID_CHARS)
|
|
37
|
+
while value > 0:
|
|
38
|
+
remainder = value % size
|
|
39
|
+
result.append(_INSTANCE_ID_CHARS[remainder])
|
|
40
|
+
value //= size
|
|
41
|
+
|
|
42
|
+
return "".join(result[::-1])
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# -----------------------------------------------------------------------------
|
|
46
|
+
# Compatibles Types
|
|
47
|
+
# -----------------------------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
_JSON_TYPES = frozenset(
|
|
50
|
+
{
|
|
51
|
+
# Common JSON Serializable types
|
|
52
|
+
types.NoneType,
|
|
53
|
+
bool,
|
|
54
|
+
int,
|
|
55
|
+
float,
|
|
56
|
+
str,
|
|
57
|
+
}
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
_COMPOSITE_TYPES = frozenset(
|
|
61
|
+
{
|
|
62
|
+
set,
|
|
63
|
+
list,
|
|
64
|
+
dict,
|
|
65
|
+
}
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
# -----------------------------------------------------------------------------
|
|
69
|
+
# Internal type definition
|
|
70
|
+
# -----------------------------------------------------------------------------
|
|
71
|
+
|
|
72
|
+
T = TypeVar("T")
|
|
73
|
+
SerializableCoreType = None | str | bool | int | float
|
|
74
|
+
SerializableType = (
|
|
75
|
+
SerializableCoreType | list[SerializableCoreType] | dict[str, SerializableCoreType]
|
|
76
|
+
)
|
|
77
|
+
Encoder = Callable[[T], SerializableType]
|
|
78
|
+
Decoder = Callable[[SerializableType], T]
|
|
79
|
+
WatcherCallback = Callable[[Any], None | Awaitable[None]]
|
|
80
|
+
|
|
81
|
+
# -----------------------------------------------------------------------------
|
|
82
|
+
# Custom Exception
|
|
83
|
+
# -----------------------------------------------------------------------------
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class NonSerializableType(ValueError):
|
|
87
|
+
pass
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class InvalidDefaultForType(ValueError):
|
|
91
|
+
pass
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class NoServerLinked(ValueError):
|
|
95
|
+
pass
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class WatcherExecution(Exception):
|
|
99
|
+
pass
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
# -----------------------------------------------------------------------------
|
|
103
|
+
# Internal classes
|
|
104
|
+
# -----------------------------------------------------------------------------
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class ContainerFactory:
|
|
108
|
+
def __init__(self, cls):
|
|
109
|
+
self._cls = cls
|
|
110
|
+
|
|
111
|
+
def __call__(self, *args, **kwargs):
|
|
112
|
+
return self._cls(*args, **kwargs)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@dataclass
|
|
116
|
+
class Watcher:
|
|
117
|
+
id: int
|
|
118
|
+
args: tuple[str]
|
|
119
|
+
dependency: set[str]
|
|
120
|
+
callback: WatcherCallback
|
|
121
|
+
sync: bool
|
|
122
|
+
|
|
123
|
+
def trigger(
|
|
124
|
+
self,
|
|
125
|
+
obj,
|
|
126
|
+
dirty: set[str] | None = None,
|
|
127
|
+
sync: bool = False,
|
|
128
|
+
eager: bool = False,
|
|
129
|
+
):
|
|
130
|
+
if self.sync != sync and not eager:
|
|
131
|
+
return
|
|
132
|
+
|
|
133
|
+
if dirty is None or self.dependency & dirty:
|
|
134
|
+
args = [getattr(obj, name) for name in self.args]
|
|
135
|
+
coroutine = self.callback(*args)
|
|
136
|
+
if inspect.isawaitable(coroutine):
|
|
137
|
+
bg_task = asyncio.create_task(coroutine)
|
|
138
|
+
bg_task.add_done_callback(handle_task_result)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def handle_task_result(task: asyncio.Task) -> None:
|
|
142
|
+
try:
|
|
143
|
+
task.result()
|
|
144
|
+
except asyncio.CancelledError:
|
|
145
|
+
pass # Task cancellation should not be logged as an error.
|
|
146
|
+
except Exception as e: # pylint: disable=broad-except
|
|
147
|
+
raise WatcherExecution() from e
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def check_loop_status():
|
|
151
|
+
try:
|
|
152
|
+
asyncio.get_running_loop()
|
|
153
|
+
return True
|
|
154
|
+
except RuntimeError:
|
|
155
|
+
return False
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
# -----------------------------------------------------------------------------
|
|
159
|
+
# Method to add to trame_dataclass
|
|
160
|
+
# -----------------------------------------------------------------------------
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _create_methods(fields, server, client, sync, valid_keys):
|
|
164
|
+
methods_to_register = {}
|
|
165
|
+
|
|
166
|
+
def __init__(self, trame_server=None, **kwargs):
|
|
167
|
+
self.__id = _next_id()
|
|
168
|
+
self.__trame_server = trame_server
|
|
169
|
+
|
|
170
|
+
# Register all instances
|
|
171
|
+
INSTANCES[self.__id] = self
|
|
172
|
+
|
|
173
|
+
self._dirty_set = set()
|
|
174
|
+
self._sync = sync
|
|
175
|
+
self._watchers = []
|
|
176
|
+
self._next_watcher_id = 1
|
|
177
|
+
self._pending_task = None
|
|
178
|
+
self._flush_impl = None
|
|
179
|
+
|
|
180
|
+
if server:
|
|
181
|
+
self._server_state = {}
|
|
182
|
+
|
|
183
|
+
if client:
|
|
184
|
+
self._client_state = {}
|
|
185
|
+
|
|
186
|
+
# set default values
|
|
187
|
+
for f in fields:
|
|
188
|
+
f.setup_instance(self)
|
|
189
|
+
|
|
190
|
+
# initialize fields from kwargs
|
|
191
|
+
self.update(**kwargs)
|
|
192
|
+
|
|
193
|
+
# register to server
|
|
194
|
+
if self.server is not None:
|
|
195
|
+
self.server.enable_module(dataclass_module)
|
|
196
|
+
if self.server.running:
|
|
197
|
+
# register protocol directly
|
|
198
|
+
self._register_server()
|
|
199
|
+
else:
|
|
200
|
+
# wait for server to be ready
|
|
201
|
+
self.server.controller.on_server_ready.add(self._register_server)
|
|
202
|
+
|
|
203
|
+
def _register_server(self, **_):
|
|
204
|
+
self.server.protocol_call("trame.dataclass.register", self)
|
|
205
|
+
|
|
206
|
+
def register_flush_implementation(self, push_function):
|
|
207
|
+
self._flush_impl = push_function
|
|
208
|
+
|
|
209
|
+
def update(self, **kwargs):
|
|
210
|
+
for key in valid_keys & set(kwargs.keys()):
|
|
211
|
+
setattr(self, key, kwargs[key])
|
|
212
|
+
|
|
213
|
+
def __repr__(self):
|
|
214
|
+
max_size = max(len(f.name) for f in fields)
|
|
215
|
+
fields_info = [
|
|
216
|
+
f"{f.name:<{max_size}} [{f.mode} | enc({'custom' if f.encoder and f.decoder else 'json'}) | {_repr_type(f.type_annotation)}: {_repr_default(f.default)} ]: {_repr_value(getattr(self, f.name))}"
|
|
217
|
+
for f in fields
|
|
218
|
+
]
|
|
219
|
+
return f"{self.__class__.__name__} ({self._id}) - {self._dirty_set if len(self._dirty_set) else 'Synched'}{os.linesep} - {f'{os.linesep} - '.join(fields_info)}"
|
|
220
|
+
|
|
221
|
+
def _on_dirty(self):
|
|
222
|
+
dirty_copy = set(self._dirty_set)
|
|
223
|
+
|
|
224
|
+
self._notify_watcher(dirty_copy, sync=True)
|
|
225
|
+
if self._pending_task is None and check_loop_status():
|
|
226
|
+
self._pending_task = asyncio.create_task(self._async_update(dirty_copy))
|
|
227
|
+
self._pending_task.add_done_callback(handle_task_result)
|
|
228
|
+
|
|
229
|
+
# only clear if you know that the dirty copy will be processed
|
|
230
|
+
# otherwise wait for completion to pickup the dirty left over.
|
|
231
|
+
self._dirty_set.clear()
|
|
232
|
+
|
|
233
|
+
if not check_loop_status():
|
|
234
|
+
# need to clear dirty if async is out of the picture
|
|
235
|
+
self._dirty_set.clear()
|
|
236
|
+
|
|
237
|
+
def _notify_watcher(self, dirty_set: set[str] | None = None, sync=False):
|
|
238
|
+
if dirty_set is None:
|
|
239
|
+
dirty_set = set(self._dirty_set)
|
|
240
|
+
|
|
241
|
+
for w in self._watchers:
|
|
242
|
+
w.trigger(self, dirty_set, sync=sync)
|
|
243
|
+
|
|
244
|
+
async def _async_update(self, dirty_set: set[str]):
|
|
245
|
+
self._notify_watcher(dirty_set, sync=False)
|
|
246
|
+
if sync:
|
|
247
|
+
self.flush(dirty_set)
|
|
248
|
+
|
|
249
|
+
self._pending_task = None
|
|
250
|
+
|
|
251
|
+
# reschedule ourself if remaining dirty
|
|
252
|
+
if self._dirty_set and check_loop_status():
|
|
253
|
+
dirty_set = set(self._dirty_set)
|
|
254
|
+
self._dirty_set.clear()
|
|
255
|
+
|
|
256
|
+
self._pending_task = asyncio.create_task(self._async_update(dirty_set))
|
|
257
|
+
self._pending_task.add_done_callback(handle_task_result)
|
|
258
|
+
|
|
259
|
+
def clear_watchers(self):
|
|
260
|
+
self._watchers.clear()
|
|
261
|
+
|
|
262
|
+
def clone(self):
|
|
263
|
+
other = self.__class__()
|
|
264
|
+
state = getattr(self, "_server_state", getattr(self, "_client_state", {}))
|
|
265
|
+
other.update(**state)
|
|
266
|
+
return other
|
|
267
|
+
|
|
268
|
+
methods_to_register["__init__"] = __init__
|
|
269
|
+
methods_to_register["update"] = update
|
|
270
|
+
methods_to_register["clone"] = clone
|
|
271
|
+
methods_to_register["__repr__"] = __repr__
|
|
272
|
+
methods_to_register["_on_dirty"] = _on_dirty
|
|
273
|
+
methods_to_register["_notify_watcher"] = _notify_watcher
|
|
274
|
+
methods_to_register["_async_update"] = _async_update
|
|
275
|
+
methods_to_register["_register_server"] = _register_server
|
|
276
|
+
methods_to_register["clear_watchers"] = clear_watchers
|
|
277
|
+
methods_to_register["register_flush_implementation"] = register_flush_implementation
|
|
278
|
+
|
|
279
|
+
# Optionally add flush method
|
|
280
|
+
if sync:
|
|
281
|
+
|
|
282
|
+
def flush(self, dirty_set: set[str] | None = None):
|
|
283
|
+
"""Flush the data to the client."""
|
|
284
|
+
if self._flush_impl is None:
|
|
285
|
+
return
|
|
286
|
+
|
|
287
|
+
if dirty_set is None:
|
|
288
|
+
dirty_set = set(self._dirty_set)
|
|
289
|
+
self._dirty_set.clear()
|
|
290
|
+
else:
|
|
291
|
+
for name in dirty_set:
|
|
292
|
+
self._dirty_set.discard(name)
|
|
293
|
+
|
|
294
|
+
fields = getattr(self.__class__, _FIELDS)
|
|
295
|
+
for name in dirty_set:
|
|
296
|
+
_save_field(fields.get(name), self, self._client_state)
|
|
297
|
+
|
|
298
|
+
# Send data over the network
|
|
299
|
+
msg = {
|
|
300
|
+
"id": self._id,
|
|
301
|
+
"state": {k: self._client_state[k] for k in dirty_set},
|
|
302
|
+
}
|
|
303
|
+
self._flush_impl(msg)
|
|
304
|
+
|
|
305
|
+
methods_to_register["flush"] = flush
|
|
306
|
+
|
|
307
|
+
return methods_to_register
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def _m_get_id(self):
|
|
311
|
+
return self.__id
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def _m_get_server(self):
|
|
315
|
+
return self.__trame_server
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def _m_set_server(self, v):
|
|
319
|
+
if self.__trame_server != v:
|
|
320
|
+
self.__trame_server = v
|
|
321
|
+
if v:
|
|
322
|
+
v.enable_module(dataclass_module)
|
|
323
|
+
self._register_server()
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def _save_field(field, src, dst):
|
|
327
|
+
if field.encoder:
|
|
328
|
+
dst[field.name] = field.encoder(getattr(src, field.name))
|
|
329
|
+
else:
|
|
330
|
+
value = getattr(src, field.name)
|
|
331
|
+
if is_trame_dataclass(value):
|
|
332
|
+
value.flush()
|
|
333
|
+
value = f"_dataclass: {value._id}"
|
|
334
|
+
dst[field.name] = value
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def _m_get_client_state(self):
|
|
338
|
+
# Make sure the client_state is fully filled
|
|
339
|
+
fields = getattr(self.__class__, _FIELDS).values()
|
|
340
|
+
dirty = set(self._dirty_set)
|
|
341
|
+
for field in fields:
|
|
342
|
+
if field.name in dirty or field.name not in self._client_state:
|
|
343
|
+
_save_field(field, self, self._client_state)
|
|
344
|
+
|
|
345
|
+
return self._client_state
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def _m_watch(
|
|
349
|
+
self,
|
|
350
|
+
field_names: tuple[str],
|
|
351
|
+
callback_func: WatcherCallback,
|
|
352
|
+
sync: bool = False,
|
|
353
|
+
eager: bool = False,
|
|
354
|
+
) -> Callable:
|
|
355
|
+
"""Register a callback to be called when one or more fields change.
|
|
356
|
+
|
|
357
|
+
Args:
|
|
358
|
+
field_names (list[str]): Name(s) of the field(s) to watch.
|
|
359
|
+
callback_func (callable): Callback function to be called when the field(s) change.
|
|
360
|
+
sync (bool): Whether to execute the callback synchronously. By default this get triggered asynchronously.
|
|
361
|
+
eager (bool): Whether to execute the callback immediately after registration.
|
|
362
|
+
|
|
363
|
+
Returns:
|
|
364
|
+
callable: Unwatch function to unregister the callback.
|
|
365
|
+
"""
|
|
366
|
+
watcher = Watcher(
|
|
367
|
+
self._next_watcher_id, field_names, set(field_names), callback_func, sync
|
|
368
|
+
)
|
|
369
|
+
self._next_watcher_id += 1
|
|
370
|
+
self._watchers.append(watcher)
|
|
371
|
+
|
|
372
|
+
def unwatch():
|
|
373
|
+
self._watchers.remove(watcher)
|
|
374
|
+
|
|
375
|
+
if eager:
|
|
376
|
+
watcher.trigger(self, eager=eager)
|
|
377
|
+
|
|
378
|
+
return unwatch
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def _m_Provider(
|
|
382
|
+
self, name: str = "data", instance: str | None = None
|
|
383
|
+
) -> TrameComponent:
|
|
384
|
+
"""Register a data provider to be used by the client.
|
|
385
|
+
|
|
386
|
+
Args:
|
|
387
|
+
name (str): Name of the data variable that will be available within the nested scope.
|
|
388
|
+
instance (str): Id of the trame_dataclass instance to use for filling the data. This behave like any other Widget property, so you can make it dynamic to switch at runtime the delivered data.
|
|
389
|
+
|
|
390
|
+
Returns:
|
|
391
|
+
widget: instance of the widget to put within your UI definition."""
|
|
392
|
+
from trame_dataclass.widgets.dataclass import Provider
|
|
393
|
+
|
|
394
|
+
if instance is None:
|
|
395
|
+
instance = (f"'{self._id}'",)
|
|
396
|
+
|
|
397
|
+
return Provider(name=name, instance=instance)
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
# -----------------------------------------------------------------------------
|
|
401
|
+
# Representation helper functions
|
|
402
|
+
# -----------------------------------------------------------------------------
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def _repr_type(annotation_type):
|
|
406
|
+
if isinstance(annotation_type, types.UnionType):
|
|
407
|
+
return f"({annotation_type})"
|
|
408
|
+
|
|
409
|
+
if is_trame_dataclass(annotation_type):
|
|
410
|
+
return annotation_type.__name__
|
|
411
|
+
|
|
412
|
+
if isinstance(annotation_type, type):
|
|
413
|
+
return annotation_type.__name__
|
|
414
|
+
|
|
415
|
+
return str(annotation_type)
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def _repr_default(value):
|
|
419
|
+
if isinstance(value, ContainerFactory):
|
|
420
|
+
return "-"
|
|
421
|
+
return _repr_value(value)
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
def _repr_value(value):
|
|
425
|
+
if is_trame_dataclass(value):
|
|
426
|
+
return "\n ".join(str(value).split("\n"))
|
|
427
|
+
if isinstance(value, str):
|
|
428
|
+
return f'"{value}"'
|
|
429
|
+
return str(value)
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
# -----------------------------------------------------------------------------
|
|
433
|
+
# Type annotation analysis helper functions
|
|
434
|
+
# -----------------------------------------------------------------------------
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
def _type_compatibility(annotation_type):
|
|
438
|
+
if annotation_type in _JSON_TYPES:
|
|
439
|
+
return True
|
|
440
|
+
|
|
441
|
+
if isinstance(annotation_type, types.UnionType):
|
|
442
|
+
return all(map(_type_compatibility, annotation_type.__args__))
|
|
443
|
+
|
|
444
|
+
if is_trame_dataclass(annotation_type):
|
|
445
|
+
return True
|
|
446
|
+
|
|
447
|
+
if annotation_type in _COMPOSITE_TYPES:
|
|
448
|
+
warnings.warn("Composite type is not templated.", stacklevel=2)
|
|
449
|
+
return True
|
|
450
|
+
|
|
451
|
+
if (
|
|
452
|
+
hasattr(annotation_type, "__origin__")
|
|
453
|
+
and annotation_type.__origin__ in _COMPOSITE_TYPES
|
|
454
|
+
):
|
|
455
|
+
return all(map(_type_compatibility, annotation_type.__args__))
|
|
456
|
+
|
|
457
|
+
return False
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
def _type_is_composite(annotation_type):
|
|
461
|
+
if annotation_type in _COMPOSITE_TYPES:
|
|
462
|
+
return True
|
|
463
|
+
|
|
464
|
+
return (
|
|
465
|
+
hasattr(annotation_type, "__origin__")
|
|
466
|
+
and annotation_type.__origin__ in _COMPOSITE_TYPES
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
def _type_can_be_none(annotation_type):
|
|
471
|
+
if isinstance(annotation_type, types.UnionType):
|
|
472
|
+
return types.NoneType in annotation_type.__args__
|
|
473
|
+
|
|
474
|
+
return False
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
def _type_is_dataclass(annotation_type):
|
|
478
|
+
if is_trame_dataclass(annotation_type):
|
|
479
|
+
return True
|
|
480
|
+
|
|
481
|
+
if isinstance(annotation_type, types.UnionType):
|
|
482
|
+
for t in annotation_type.__args__:
|
|
483
|
+
if is_trame_dataclass(t):
|
|
484
|
+
return True
|
|
485
|
+
|
|
486
|
+
return False
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
def _type_default(annotation_type):
|
|
490
|
+
if _type_can_be_none(annotation_type):
|
|
491
|
+
return None
|
|
492
|
+
|
|
493
|
+
if annotation_type is int:
|
|
494
|
+
return 0
|
|
495
|
+
|
|
496
|
+
if annotation_type is float:
|
|
497
|
+
return 0.0
|
|
498
|
+
|
|
499
|
+
if annotation_type is bool:
|
|
500
|
+
return False
|
|
501
|
+
|
|
502
|
+
if annotation_type is str:
|
|
503
|
+
return ""
|
|
504
|
+
|
|
505
|
+
if _type_is_composite(annotation_type):
|
|
506
|
+
container_type = (
|
|
507
|
+
annotation_type.__origin__
|
|
508
|
+
if hasattr(annotation_type, "__origin__")
|
|
509
|
+
else annotation_type
|
|
510
|
+
)
|
|
511
|
+
if container_type is list:
|
|
512
|
+
return ContainerFactory(list)
|
|
513
|
+
if container_type is set:
|
|
514
|
+
return ContainerFactory(set)
|
|
515
|
+
if container_type is dict:
|
|
516
|
+
return ContainerFactory(dict)
|
|
517
|
+
raise InvalidDefaultForType(annotation_type)
|
|
518
|
+
|
|
519
|
+
if isinstance(annotation_type, types.GenericAlias):
|
|
520
|
+
return _type_default(annotation_type.__origin__)
|
|
521
|
+
|
|
522
|
+
if is_trame_dataclass(annotation_type):
|
|
523
|
+
return ContainerFactory(annotation_type)
|
|
524
|
+
|
|
525
|
+
raise InvalidDefaultForType(annotation_type)
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
# -----------------------------------------------------------------------------
|
|
529
|
+
# Dataclass builder
|
|
530
|
+
# -----------------------------------------------------------------------------
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
def _process_class(cls):
|
|
534
|
+
cls_annotations = cls.__dict__.get("__annotations__", {})
|
|
535
|
+
cls_fields = []
|
|
536
|
+
for name, type in cls_annotations.items():
|
|
537
|
+
initial_value = cls.__dict__.get(name, None)
|
|
538
|
+
if initial_value is not None and isinstance(initial_value, Field):
|
|
539
|
+
initial_value.setup_annotation(name, type)
|
|
540
|
+
cls_fields.append(initial_value)
|
|
541
|
+
else:
|
|
542
|
+
if not _type_compatibility(type):
|
|
543
|
+
msg = f"{type} is not supported"
|
|
544
|
+
raise NonSerializableType(msg)
|
|
545
|
+
|
|
546
|
+
field = Field(default=initial_value)
|
|
547
|
+
field.setup_annotation(name, type)
|
|
548
|
+
cls_fields.append(field)
|
|
549
|
+
|
|
550
|
+
# add class metadata
|
|
551
|
+
setattr(cls, _FIELDS, {f.name: f for f in cls_fields})
|
|
552
|
+
for f in cls_fields:
|
|
553
|
+
f.setup_class(cls)
|
|
554
|
+
|
|
555
|
+
# Extract field meta summary
|
|
556
|
+
server = any(f.mode.has_server_state for f in cls_fields)
|
|
557
|
+
client = any(f.mode.has_client_state for f in cls_fields)
|
|
558
|
+
sync = any(f.mode.need_sync for f in cls_fields)
|
|
559
|
+
valid_keys = {f.name for f in cls_fields}
|
|
560
|
+
|
|
561
|
+
# add default getter properties
|
|
562
|
+
cls._id = property(_m_get_id)
|
|
563
|
+
cls.server = property(_m_get_server, _m_set_server)
|
|
564
|
+
if sync:
|
|
565
|
+
cls.client_state = property(_m_get_client_state)
|
|
566
|
+
|
|
567
|
+
# Add default methods
|
|
568
|
+
for name, fn in _create_methods(
|
|
569
|
+
cls_fields, server, client, sync, valid_keys
|
|
570
|
+
).items():
|
|
571
|
+
setattr(cls, name, fn)
|
|
572
|
+
cls.watch = _m_watch
|
|
573
|
+
cls.Provider = _m_Provider
|
|
574
|
+
|
|
575
|
+
# return decorated class
|
|
576
|
+
return cls
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
# -----------------------------------------------------------------------------
|
|
580
|
+
# Generic encoder/decoder
|
|
581
|
+
# -----------------------------------------------------------------------------
|
|
582
|
+
|
|
583
|
+
|
|
584
|
+
def encode_dataclass_item(item):
|
|
585
|
+
if item is None:
|
|
586
|
+
return None
|
|
587
|
+
return item._id
|
|
588
|
+
|
|
589
|
+
|
|
590
|
+
def decode_dataclass_item(item):
|
|
591
|
+
# print("decode_dataclass_item", item)
|
|
592
|
+
if item is None:
|
|
593
|
+
return None
|
|
594
|
+
return get_instance(item)
|
|
595
|
+
|
|
596
|
+
|
|
597
|
+
def encode_dataclass_list(items):
|
|
598
|
+
return [item._id for item in items]
|
|
599
|
+
|
|
600
|
+
|
|
601
|
+
def decode_dataclass_list(items):
|
|
602
|
+
# print("decode_dataclass_list", items)
|
|
603
|
+
return list(map(get_instance, items))
|
|
604
|
+
|
|
605
|
+
|
|
606
|
+
def encode_dataclass_dict(data):
|
|
607
|
+
return {k: v._id for k, v in data.items()}
|
|
608
|
+
|
|
609
|
+
|
|
610
|
+
def decode_dataclass_dict(data):
|
|
611
|
+
# print("decode_dataclass_dict", data)
|
|
612
|
+
return {k: get_instance(v) for k, v in data.items()}
|
|
613
|
+
|
|
614
|
+
|
|
615
|
+
# -----------------------------------------------------------------------------
|
|
616
|
+
# Public API
|
|
617
|
+
# -----------------------------------------------------------------------------
|
|
618
|
+
__all__ = [
|
|
619
|
+
"Field",
|
|
620
|
+
"FieldMode",
|
|
621
|
+
"get_instance",
|
|
622
|
+
"is_trame_dataclass",
|
|
623
|
+
"trame_dataclass",
|
|
624
|
+
]
|
|
625
|
+
|
|
626
|
+
|
|
627
|
+
def get_instance(instance_id: str):
|
|
628
|
+
# print(f"get_instance({instance_id})")
|
|
629
|
+
# print(" => ", INSTANCES[instance_id])
|
|
630
|
+
return INSTANCES[instance_id]
|
|
631
|
+
|
|
632
|
+
|
|
633
|
+
def trame_dataclass(cls=None, **_):
|
|
634
|
+
"""Annotation for state based dataclass"""
|
|
635
|
+
|
|
636
|
+
def wrap(cls):
|
|
637
|
+
return _process_class(cls)
|
|
638
|
+
|
|
639
|
+
if cls is None:
|
|
640
|
+
return wrap
|
|
641
|
+
|
|
642
|
+
return wrap(cls)
|
|
643
|
+
|
|
644
|
+
|
|
645
|
+
def is_trame_dataclass(obj):
|
|
646
|
+
"""Returns True if obj is a trame_dataclass or an instance of a
|
|
647
|
+
trame_dataclass."""
|
|
648
|
+
cls = (
|
|
649
|
+
obj
|
|
650
|
+
if isinstance(obj, type) and not isinstance(obj, types.GenericAlias)
|
|
651
|
+
else type(obj)
|
|
652
|
+
)
|
|
653
|
+
return hasattr(cls, _FIELDS)
|
|
654
|
+
|
|
655
|
+
|
|
656
|
+
class FieldMode(Enum):
|
|
657
|
+
CLIENT_ONLY = (False, False, True)
|
|
658
|
+
READ_ONLY = (True, False, True)
|
|
659
|
+
PUSH_ONLY = (False, True, True)
|
|
660
|
+
SERVER_ONLY = (True, True, False)
|
|
661
|
+
DEFAULT = (True, True, True)
|
|
662
|
+
|
|
663
|
+
def __init__(self, server_get, server_set, client):
|
|
664
|
+
self._value_ = auto()
|
|
665
|
+
self._get = server_get
|
|
666
|
+
self._set = server_set
|
|
667
|
+
self._client = client
|
|
668
|
+
|
|
669
|
+
@property
|
|
670
|
+
def has_get(self):
|
|
671
|
+
return self._get or self._set
|
|
672
|
+
|
|
673
|
+
@property
|
|
674
|
+
def has_set(self):
|
|
675
|
+
return self._set
|
|
676
|
+
|
|
677
|
+
@property
|
|
678
|
+
def has_server_state(self):
|
|
679
|
+
return self._get or self._set
|
|
680
|
+
|
|
681
|
+
@property
|
|
682
|
+
def has_client_state(self):
|
|
683
|
+
return self._client
|
|
684
|
+
|
|
685
|
+
@property
|
|
686
|
+
def need_sync(self):
|
|
687
|
+
return self.has_server_state and self.has_client_state
|
|
688
|
+
|
|
689
|
+
|
|
690
|
+
# -----------------------------------------------------------------------------
|
|
691
|
+
|
|
692
|
+
|
|
693
|
+
class Field:
|
|
694
|
+
def __init__(
|
|
695
|
+
self,
|
|
696
|
+
mode: FieldMode = FieldMode.DEFAULT,
|
|
697
|
+
default: Any = None,
|
|
698
|
+
encoder: Encoder | None = None,
|
|
699
|
+
decoder: Decoder | None = None,
|
|
700
|
+
):
|
|
701
|
+
self.name = None
|
|
702
|
+
self.type_annotation = None
|
|
703
|
+
self.mode = mode
|
|
704
|
+
self.default = default
|
|
705
|
+
self.encoder = encoder
|
|
706
|
+
self.decoder = decoder
|
|
707
|
+
self.dataclass_container = False
|
|
708
|
+
|
|
709
|
+
def setup_annotation(self, name, type_annotation):
|
|
710
|
+
self.name = name
|
|
711
|
+
self.type_annotation = type_annotation
|
|
712
|
+
if self.default is None:
|
|
713
|
+
self.default = _type_default(type_annotation)
|
|
714
|
+
|
|
715
|
+
if _type_is_composite(type_annotation):
|
|
716
|
+
if hasattr(type_annotation, "__origin__"):
|
|
717
|
+
# properly typed
|
|
718
|
+
if type_annotation.__origin__ is list and is_trame_dataclass(
|
|
719
|
+
type_annotation.__args__[0]
|
|
720
|
+
):
|
|
721
|
+
# print("Use custom list[dataclass] encoder/decoder")
|
|
722
|
+
assert self.encoder is None, (
|
|
723
|
+
"DataClass encoder get managed automatically. Should not override an existing one!"
|
|
724
|
+
)
|
|
725
|
+
self.encoder = encode_dataclass_list
|
|
726
|
+
self.decoder = decode_dataclass_list
|
|
727
|
+
self.dataclass_container = True
|
|
728
|
+
elif type_annotation.__origin__ is dict and is_trame_dataclass(
|
|
729
|
+
type_annotation.__args__[1]
|
|
730
|
+
):
|
|
731
|
+
assert type_annotation.__args__[0] is str, (
|
|
732
|
+
"Dict with dataclass must use str as key"
|
|
733
|
+
)
|
|
734
|
+
assert self.encoder is None, (
|
|
735
|
+
"DataClass encoder get managed automatically. Should not override an existing one!"
|
|
736
|
+
)
|
|
737
|
+
# print("Use custom dict[str, dataclass] encoder/decoder")
|
|
738
|
+
self.encoder = encode_dataclass_dict
|
|
739
|
+
self.decoder = decode_dataclass_dict
|
|
740
|
+
self.dataclass_container = True
|
|
741
|
+
elif _type_is_dataclass(type_annotation):
|
|
742
|
+
assert self.encoder is None, (
|
|
743
|
+
"DataClass encoder get managed automatically. Should not override an existing one!"
|
|
744
|
+
)
|
|
745
|
+
self.encoder = encode_dataclass_item
|
|
746
|
+
self.decoder = decode_dataclass_item
|
|
747
|
+
self.dataclass_container = True
|
|
748
|
+
|
|
749
|
+
def setup_class(self, cls):
|
|
750
|
+
"""Patch class with methods to add"""
|
|
751
|
+
name = self.name
|
|
752
|
+
|
|
753
|
+
def _set(self, value):
|
|
754
|
+
if name in self._server_state and self._server_state[name] == value:
|
|
755
|
+
return
|
|
756
|
+
|
|
757
|
+
self._dirty_set.add(name)
|
|
758
|
+
self._server_state[name] = value
|
|
759
|
+
self._on_dirty()
|
|
760
|
+
|
|
761
|
+
def _get(self):
|
|
762
|
+
return self._server_state[name]
|
|
763
|
+
|
|
764
|
+
if self.mode.has_get and self.mode.has_set:
|
|
765
|
+
setattr(cls, name, property(_get, _set))
|
|
766
|
+
elif self.mode.has_get:
|
|
767
|
+
setattr(cls, name, property(_get))
|
|
768
|
+
|
|
769
|
+
def setup_instance(self, obj):
|
|
770
|
+
# Assign value
|
|
771
|
+
value = self.default() if callable(self.default) else self.default
|
|
772
|
+
if self.mode.has_set:
|
|
773
|
+
setattr(obj, self.name, value)
|
|
774
|
+
elif self.mode.has_client_state:
|
|
775
|
+
obj._client_state[self.name] = value
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
# Compute local path to serve
|
|
4
|
+
serve_path = str(Path(__file__).with_name("serve").resolve())
|
|
5
|
+
|
|
6
|
+
# Serve directory for JS/CSS files
|
|
7
|
+
serve = {"__trame_dataclass": serve_path}
|
|
8
|
+
|
|
9
|
+
# List of JS files to load (usually from the serve path above)
|
|
10
|
+
scripts = ["__trame_dataclass/trame_dataclass.umd.js"]
|
|
11
|
+
|
|
12
|
+
# List of CSS files to load (usually from the serve path above)
|
|
13
|
+
if (Path(serve_path) / "style.css").exists():
|
|
14
|
+
styles = ["__trame_dataclass/style.css"]
|
|
15
|
+
|
|
16
|
+
# List of Vue plugins to install/load
|
|
17
|
+
vue_use = ["trame_dataclass"]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# Optional if you want to execute custom initialization at module load
|
|
21
|
+
def setup(server, **_):
|
|
22
|
+
"""Method called at initialization with possibly some custom keyword arguments"""
|
|
23
|
+
server.add_protocol_to_configure(configure_protocol)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def configure_protocol(protocol):
|
|
27
|
+
from trame_dataclass.module.protocol import TrameDataclassProtocol
|
|
28
|
+
|
|
29
|
+
protocol.registerLinkProtocol(TrameDataclassProtocol())
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
from wslink import register as export_rpc
|
|
2
|
+
from wslink.websocket import LinkProtocol
|
|
3
|
+
|
|
4
|
+
from trame_dataclass.core import get_instance, is_trame_dataclass
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def compute_definition(trame_dataclass_class):
|
|
8
|
+
return {
|
|
9
|
+
"name": trame_dataclass_class.__name__,
|
|
10
|
+
"dataclass_containers": [
|
|
11
|
+
f.name
|
|
12
|
+
for f in trame_dataclass_class.__trame_dataclass_fields__.values()
|
|
13
|
+
if f.dataclass_container
|
|
14
|
+
],
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class TrameDataclassProtocol(LinkProtocol):
|
|
19
|
+
def __init__(self, *args, **kwargs):
|
|
20
|
+
super().__init__(*args, **kwargs)
|
|
21
|
+
|
|
22
|
+
self.class_definitions = {}
|
|
23
|
+
self.next_class_definition_id = 1
|
|
24
|
+
|
|
25
|
+
@export_rpc("trame.dataclass.register")
|
|
26
|
+
def register_instance(self, trame_dataclass):
|
|
27
|
+
if is_trame_dataclass(trame_dataclass):
|
|
28
|
+
self.register_definition(trame_dataclass.__class__)
|
|
29
|
+
trame_dataclass.register_flush_implementation(self.push_delta)
|
|
30
|
+
|
|
31
|
+
def register_definition(self, trame_dataclass_class):
|
|
32
|
+
if not is_trame_dataclass(trame_dataclass_class):
|
|
33
|
+
return None
|
|
34
|
+
|
|
35
|
+
if trame_dataclass_class in self.class_definitions:
|
|
36
|
+
return self.class_definitions[trame_dataclass_class]
|
|
37
|
+
|
|
38
|
+
definition_id = self.next_class_definition_id
|
|
39
|
+
self.next_class_definition_id += 1
|
|
40
|
+
|
|
41
|
+
definition = {
|
|
42
|
+
"id": definition_id,
|
|
43
|
+
**compute_definition(trame_dataclass_class),
|
|
44
|
+
}
|
|
45
|
+
self.class_definitions[trame_dataclass_class] = definition
|
|
46
|
+
|
|
47
|
+
return definition
|
|
48
|
+
|
|
49
|
+
@export_rpc("trame.dataclass.definition.get")
|
|
50
|
+
def get_definition(self, class_id):
|
|
51
|
+
for definition in self.class_definitions.values():
|
|
52
|
+
if definition["id"] == class_id:
|
|
53
|
+
return definition
|
|
54
|
+
return None
|
|
55
|
+
|
|
56
|
+
@export_rpc("trame.dataclass.state.get")
|
|
57
|
+
def get_state(self, instance_id):
|
|
58
|
+
"""
|
|
59
|
+
{
|
|
60
|
+
id: instance_id,
|
|
61
|
+
definition: class_id,
|
|
62
|
+
state: {},
|
|
63
|
+
}
|
|
64
|
+
"""
|
|
65
|
+
obj = get_instance(instance_id)
|
|
66
|
+
if obj is None:
|
|
67
|
+
return {
|
|
68
|
+
"id": instance_id,
|
|
69
|
+
"state": None,
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
"id": instance_id,
|
|
74
|
+
"definition": self.class_definitions[obj.__class__]["id"],
|
|
75
|
+
"state": obj.client_state,
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
@export_rpc("trame.dataclass.state.update")
|
|
79
|
+
def update_state(self, msg):
|
|
80
|
+
# print("update_state", msg)
|
|
81
|
+
for dc_id, state in msg.items():
|
|
82
|
+
obj = get_instance(dc_id)
|
|
83
|
+
fields = obj.__class__.__trame_dataclass_fields__
|
|
84
|
+
if obj is not None:
|
|
85
|
+
for k, v in state.items():
|
|
86
|
+
field = fields.get(k)
|
|
87
|
+
if field.decoder:
|
|
88
|
+
setattr(obj, k, field.decoder(v))
|
|
89
|
+
else:
|
|
90
|
+
setattr(obj, k, v)
|
|
91
|
+
|
|
92
|
+
def push_delta(self, msg):
|
|
93
|
+
# print("publish", msg)
|
|
94
|
+
self.publish("trame.dataclass.publish", msg)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
(function(r,c){typeof exports=="object"&&typeof module<"u"?c(exports,require("vue")):typeof define=="function"&&define.amd?define(["exports","vue"],c):(r=typeof globalThis<"u"?globalThis:r||self,c(r.trame_dataclass={},r.Vue))})(this,function(r,c){"use strict";function f(h,t,e){if(e.data._id!==h){e.data._id=h;const s=Object.keys(e.data);for(let i of s)delete e.data[i]}Object.assign(e.data,t.refs)}class d{constructor(){this.client=null,this.subscription=null,this.dataStates={},this.dataTypes={},this.typeDefinitions={},this.vueComponents={},this.internalReactiveObjects={},this.dataToVue={},this.pendingClientServerQueue=[],this.pendingFlushRequest=0}connect(t){this.client||!t||(this.client=t,this.subscription=t.getConnection().getSession().subscribe("trame.dataclass.publish",async([e])=>{const{id:s,state:i}=e;Object.assign(this.dataStates[s].server,i);for(const[n,a]of Object.entries(i))this.isDataClass(s,n)?await this.handleNestedDataClass(s,n,a):this.dataStates[s].refs[n].value=a}))}updateServer(t,e,s){this.pendingClientServerQueue.push([t,e,s]),this.flushToServer()}async flushToServer(){if(this.pendingFlushRequest)return;this.pendingFlushRequest++;const t={};let e=0;for(;this.pendingClientServerQueue.length;){const[s,i,n]=this.pendingClientServerQueue.shift();let a=n;if(n!==null&&this.isDataClass(s,i))if(Array.isArray(n))a=n.map(o=>o._id);else if(n._id)a=n._id;else{a={};for(const[o,v]in Object.entries(n))a[o]=v._id}JSON.stringify(this.dataStates[s].server[i])!==JSON.stringify(a)&&(t[s]||(t[s]={}),t[s][i]=a,e++)}if(e)try{await this.client.getConnection().getSession().call("trame.dataclass.state.update",[t])}catch(s){console.error("Network error when pushing client state",s)}this.pendingFlushRequest--,this.pendingClientServerQueue.length&&this.flushToServer()}isDataClass(t,e){return this.typeDefinitions[this.dataTypes[t]].dataclass_containers.includes(e)}async handleNestedDataClass(t,e,s){if(s===null)this.dataStates[t].refs[e].value=null;else if(Array.isArray(s)){const i=[];for(let n=0;n<s.length;n++){const a=s[n];let o=!1;this.internalReactiveObjects[a]||(this.internalReactiveObjects[a]=c.reactive({}),o=!0),i.push(this.internalReactiveObjects[a]),this.dataStates[a]||(o=!1,await this.fetchState(a)),o&&Object.assign(this.internalReactiveObjects[a],this.dataStates[a].refs)}this.dataStates[t].refs[e].value=i}else if(typeof s=="string"){const i=s;this.internalReactiveObjects[i]||(this.internalReactiveObjects[i]=c.reactive({})),this.dataStates[i]?Object.assign(this.internalReactiveObjects[i],this.dataStates[i].refs):await this.fetchState(i)}else{const i=c.reactive({});for(const[n,a]of Object.entries(s))this.internalReactiveObjects[a]||(this.internalReactiveObjects[a]=c.reactive({})),i[n]=this.internalReactiveObjects[a],this.dataStates[a]?Object.assign(this.internalReactiveObjects[a],this.dataStates[a].refs):await this.fetchState(a);this.dataStates[t].refs[e].value=i}}async fetchState(t){const e={_id:t},s=await this.client.getConnection().getSession().call("trame.dataclass.state.get",[t]);this.dataTypes[t]=s.definition,this.dataStates[t]={refs:e,server:s.state},this.typeDefinitions[s.definition]||await this.fetchDefinition(s.definition);for(const[i,n]of Object.entries(s.state))this.isDataClass(t,i)?(e[i]=c.ref(null),await this.handleNestedDataClass(t,i,n)):e[i]=c.ref(n),c.watch(()=>e[i].value,a=>this.updateServer(t,i,a));return this.dataToVue[t]&&this.dataToVue[t].forEach(i=>{f(t,this.dataStates[t],this.vueComponents[i])}),this.internalReactiveObjects[t]&&Object.assign(this.internalReactiveObjects[t],this.dataStates[t].refs),this.dataStates[t]}async fetchDefinition(t){const e=await this.client.getConnection().getSession().call("trame.dataclass.definition.get",[t]);this.typeDefinitions[t]=e}unlink(t,e){this.dataToVue[t]&&(this.dataToVue[t]=this.dataToVue[t].filter(s=>s!==e))}link(t,e){this.dataToVue[t]?this.dataToVue[t].push(e):this.dataToVue[t]=[e],this.dataStates[t]?f(t,this.dataStates[t],this.vueComponents[e]):this.fetchState(t)}connectVueComponent(t,e){var n;const s=(n=this.vueComponents[t])==null?void 0:n.id,i=e.id;this.unlink(s,t),this.vueComponents[t]=e,this.link(i,t)}disconnectVueComponent(t){var s;const e=(s=this.vueComponents[t])==null?void 0:s.id;delete this.vueComponents[t],this.unlink(e,t)}}const l=new d;let p=1;const u={TrameDataclass:{props:["instance"],setup(h){const t=c.inject("trame"),e=c.reactive({}),s={serverPush:!1},i=`vueDataClass${p++}`;return l.connect(t.client),c.watchEffect(()=>{l.connectVueComponent(i,{id:h.instance,data:e,guards:s})}),c.onBeforeUnmount(()=>{l.disconnectVueComponent(i)}),{data:e}},template:'<slot :dataclass="data"></slot>'}};function S(h){Object.keys(u).forEach(t=>{h.component(t,u[t])})}r.install=S,Object.defineProperty(r,Symbol.toStringTag,{value:"Module"})});
|
|
File without changes
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from trame_client.widgets.core import AbstractElement
|
|
2
|
+
|
|
3
|
+
from .. import module
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class HtmlElement(AbstractElement):
|
|
7
|
+
def __init__(self, _elem_name, children=None, **kwargs):
|
|
8
|
+
super().__init__(_elem_name, children, **kwargs)
|
|
9
|
+
if self.server:
|
|
10
|
+
self.server.enable_module(module)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"Provider",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# Expose your vue component(s)
|
|
19
|
+
class Provider(HtmlElement):
|
|
20
|
+
def __init__(self, name, **kwargs):
|
|
21
|
+
super().__init__(
|
|
22
|
+
"trame-dataclass",
|
|
23
|
+
**kwargs,
|
|
24
|
+
)
|
|
25
|
+
self._attr_names += ["instance"]
|
|
26
|
+
self._attributes["slot"] = f'v-slot="{{ dataclass: {name} }}"'
|