pyglove 0.4.5.dev202411132359__py3-none-any.whl → 0.4.5.dev202501250807__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.
- pyglove/core/__init__.py +40 -21
- pyglove/core/coding/__init__.py +42 -0
- pyglove/core/coding/errors.py +111 -0
- pyglove/core/coding/errors_test.py +98 -0
- pyglove/core/coding/execution.py +312 -0
- pyglove/core/coding/execution_test.py +333 -0
- pyglove/core/{object_utils/codegen.py → coding/function_generation.py} +10 -4
- pyglove/core/{object_utils/codegen_test.py → coding/function_generation_test.py} +5 -7
- pyglove/core/coding/parsing.py +153 -0
- pyglove/core/coding/parsing_test.py +150 -0
- pyglove/core/coding/permissions.py +100 -0
- pyglove/core/coding/permissions_test.py +93 -0
- pyglove/core/geno/base.py +53 -38
- pyglove/core/geno/base_test.py +2 -4
- pyglove/core/geno/categorical.py +36 -27
- pyglove/core/geno/custom.py +18 -15
- pyglove/core/geno/numerical.py +19 -16
- pyglove/core/geno/space.py +3 -4
- pyglove/core/hyper/base.py +6 -6
- pyglove/core/hyper/categorical.py +91 -52
- pyglove/core/hyper/custom.py +7 -7
- pyglove/core/hyper/custom_test.py +9 -10
- pyglove/core/hyper/derived.py +30 -22
- pyglove/core/hyper/derived_test.py +3 -5
- pyglove/core/hyper/dynamic_evaluation.py +3 -4
- pyglove/core/hyper/evolvable.py +57 -46
- pyglove/core/hyper/numerical.py +48 -24
- pyglove/core/hyper/numerical_test.py +9 -9
- pyglove/core/hyper/object_template.py +58 -46
- pyglove/core/logging_test.py +0 -2
- pyglove/core/patching/object_factory.py +4 -4
- pyglove/core/patching/pattern_based.py +4 -4
- pyglove/core/patching/rule_based.py +4 -3
- pyglove/core/symbolic/__init__.py +4 -0
- pyglove/core/symbolic/base.py +200 -136
- pyglove/core/symbolic/base_test.py +17 -19
- pyglove/core/symbolic/boilerplate.py +4 -5
- pyglove/core/symbolic/class_wrapper.py +10 -14
- pyglove/core/symbolic/class_wrapper_test.py +2 -2
- pyglove/core/symbolic/compounding.py +2 -2
- pyglove/core/symbolic/compounding_test.py +2 -4
- pyglove/core/symbolic/contextual_object.py +288 -0
- pyglove/core/symbolic/contextual_object_test.py +327 -0
- pyglove/core/symbolic/dict.py +115 -87
- pyglove/core/symbolic/dict_test.py +188 -131
- pyglove/core/symbolic/diff.py +12 -12
- pyglove/core/symbolic/flags.py +1 -1
- pyglove/core/symbolic/functor.py +16 -15
- pyglove/core/symbolic/functor_test.py +2 -4
- pyglove/core/symbolic/inferred.py +2 -2
- pyglove/core/symbolic/list.py +70 -47
- pyglove/core/symbolic/list_test.py +117 -98
- pyglove/core/symbolic/object.py +59 -58
- pyglove/core/symbolic/object_test.py +143 -90
- pyglove/core/symbolic/origin.py +5 -7
- pyglove/core/symbolic/pure_symbolic.py +4 -3
- pyglove/core/symbolic/ref.py +33 -16
- pyglove/core/symbolic/ref_test.py +17 -0
- pyglove/core/tuning/local_backend.py +2 -2
- pyglove/core/tuning/protocols.py +3 -3
- pyglove/core/typing/annotation_conversion.py +8 -3
- pyglove/core/typing/annotation_conversion_test.py +8 -0
- pyglove/core/typing/callable_ext.py +11 -13
- pyglove/core/typing/callable_signature.py +22 -19
- pyglove/core/typing/callable_signature_test.py +3 -5
- pyglove/core/typing/class_schema.py +93 -54
- pyglove/core/typing/class_schema_test.py +4 -5
- pyglove/core/typing/custom_typing.py +5 -4
- pyglove/core/typing/key_specs.py +5 -7
- pyglove/core/typing/key_specs_test.py +4 -4
- pyglove/core/typing/type_conversion.py +4 -5
- pyglove/core/typing/type_conversion_test.py +12 -12
- pyglove/core/typing/typed_missing.py +6 -7
- pyglove/core/typing/typed_missing_test.py +7 -8
- pyglove/core/typing/value_specs.py +287 -144
- pyglove/core/typing/value_specs_test.py +148 -25
- pyglove/core/utils/__init__.py +172 -0
- pyglove/core/{object_utils → utils}/common_traits.py +2 -2
- pyglove/core/{object_utils → utils}/common_traits_test.py +1 -3
- pyglove/core/utils/contextual.py +147 -0
- pyglove/core/utils/contextual_test.py +88 -0
- pyglove/core/{object_utils → utils}/docstr_utils_test.py +1 -3
- pyglove/core/{object_utils → utils}/error_utils.py +3 -3
- pyglove/core/{object_utils → utils}/error_utils_test.py +1 -1
- pyglove/core/{object_utils → utils}/formatting.py +1 -1
- pyglove/core/{object_utils → utils}/formatting_test.py +1 -2
- pyglove/core/{object_utils → utils}/hierarchical.py +23 -25
- pyglove/core/{object_utils → utils}/hierarchical_test.py +3 -5
- pyglove/core/{object_utils → utils}/json_conversion.py +1 -1
- pyglove/core/{object_utils → utils}/json_conversion_test.py +1 -3
- pyglove/core/{object_utils → utils}/missing.py +2 -2
- pyglove/core/{object_utils → utils}/missing_test.py +2 -4
- pyglove/core/utils/text_color.py +128 -0
- pyglove/core/utils/text_color_test.py +94 -0
- pyglove/core/{object_utils → utils}/thread_local_test.py +1 -3
- pyglove/core/{object_utils → utils}/timing.py +21 -10
- pyglove/core/{object_utils → utils}/timing_test.py +14 -12
- pyglove/core/{object_utils → utils}/value_location.py +2 -2
- pyglove/core/{object_utils → utils}/value_location_test.py +2 -4
- pyglove/core/views/base.py +25 -29
- pyglove/core/views/html/base.py +15 -16
- pyglove/core/views/html/controls/base.py +46 -9
- pyglove/core/views/html/controls/label.py +13 -2
- pyglove/core/views/html/controls/label_test.py +27 -8
- pyglove/core/views/html/controls/progress_bar.py +3 -5
- pyglove/core/views/html/controls/progress_bar_test.py +2 -2
- pyglove/core/views/html/controls/tab.py +217 -66
- pyglove/core/views/html/controls/tab_test.py +46 -15
- pyglove/core/views/html/tree_view.py +39 -37
- {pyglove-0.4.5.dev202411132359.dist-info → pyglove-0.4.5.dev202501250807.dist-info}/METADATA +17 -3
- pyglove-0.4.5.dev202501250807.dist-info/RECORD +218 -0
- {pyglove-0.4.5.dev202411132359.dist-info → pyglove-0.4.5.dev202501250807.dist-info}/WHEEL +1 -1
- pyglove/core/object_utils/__init__.py +0 -164
- pyglove-0.4.5.dev202411132359.dist-info/RECORD +0 -203
- /pyglove/core/{object_utils → utils}/docstr_utils.py +0 -0
- /pyglove/core/{object_utils → utils}/thread_local.py +0 -0
- {pyglove-0.4.5.dev202411132359.dist-info → pyglove-0.4.5.dev202501250807.dist-info}/LICENSE +0 -0
- {pyglove-0.4.5.dev202411132359.dist-info → pyglove-0.4.5.dev202501250807.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,94 @@
|
|
1
|
+
# Copyright 2025 The PyGlove Authors
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at
|
6
|
+
#
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
#
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
# See the License for the specific language governing permissions and
|
13
|
+
# limitations under the License.
|
14
|
+
import inspect
|
15
|
+
import os
|
16
|
+
import unittest
|
17
|
+
from pyglove.core.utils import text_color
|
18
|
+
|
19
|
+
|
20
|
+
class TextColorTest(unittest.TestCase):
|
21
|
+
|
22
|
+
def setUp(self):
|
23
|
+
super().setUp()
|
24
|
+
os.environ.pop('ANSI_COLORS_DISABLED', None)
|
25
|
+
os.environ.pop('NO_COLOR', None)
|
26
|
+
os.environ['FORCE_COLOR'] = '1'
|
27
|
+
|
28
|
+
def test_colored_block_without_colors_and_styles(self):
|
29
|
+
self.assertEqual(text_color.colored_block('foo', '{{', '}}'), 'foo')
|
30
|
+
|
31
|
+
def test_colored_block(self):
|
32
|
+
original_text = inspect.cleandoc("""
|
33
|
+
Hi << foo >>
|
34
|
+
<# print x if x is present #>
|
35
|
+
<% if x %>
|
36
|
+
<< x >>
|
37
|
+
<% endif %>
|
38
|
+
""")
|
39
|
+
|
40
|
+
colored_text = text_color.colored_block(
|
41
|
+
text_color.colored(original_text, color='blue'),
|
42
|
+
'<<', '>>',
|
43
|
+
color='white',
|
44
|
+
background='blue',
|
45
|
+
)
|
46
|
+
origin_color = '\x1b[34m'
|
47
|
+
reset = '\x1b[0m'
|
48
|
+
block_color = text_color.colored(
|
49
|
+
'TEXT', color='white', background='blue'
|
50
|
+
).split('TEXT')[0]
|
51
|
+
self.assertEqual(
|
52
|
+
colored_text,
|
53
|
+
f'{origin_color}Hi {block_color}<< foo >>{reset}{origin_color}\n'
|
54
|
+
'<# print x if x is present #>\n<% if x %>\n'
|
55
|
+
f'{block_color}<< x >>{reset}{origin_color}\n'
|
56
|
+
f'<% endif %>{reset}'
|
57
|
+
)
|
58
|
+
self.assertEqual(text_color.decolor(colored_text), original_text)
|
59
|
+
|
60
|
+
def test_colored_block_without_full_match(self):
|
61
|
+
self.assertEqual(
|
62
|
+
text_color.colored_block(
|
63
|
+
'Hi {{ foo',
|
64
|
+
'{{', '}}',
|
65
|
+
color='white',
|
66
|
+
background='blue',
|
67
|
+
),
|
68
|
+
'Hi {{ foo'
|
69
|
+
)
|
70
|
+
|
71
|
+
def test_colored_block_without_termcolor(self):
|
72
|
+
termcolor = text_color.termcolor
|
73
|
+
text_color.termcolor = None
|
74
|
+
original_text = inspect.cleandoc("""
|
75
|
+
Hi {{ foo }}
|
76
|
+
{# print x if x is present #}
|
77
|
+
{% if x %}
|
78
|
+
{{ x }}
|
79
|
+
{% endif %}
|
80
|
+
""")
|
81
|
+
|
82
|
+
colored_text = text_color.colored_block(
|
83
|
+
text_color.colored(original_text, color='blue'),
|
84
|
+
'{{', '}}',
|
85
|
+
color='white',
|
86
|
+
background='blue',
|
87
|
+
)
|
88
|
+
self.assertEqual(colored_text, original_text)
|
89
|
+
self.assertEqual(text_color.decolor(colored_text), original_text)
|
90
|
+
text_color.termcolor = termcolor
|
91
|
+
|
92
|
+
|
93
|
+
if __name__ == '__main__':
|
94
|
+
unittest.main()
|
@@ -11,13 +11,11 @@
|
|
11
11
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
12
|
# See the License for the specific language governing permissions and
|
13
13
|
# limitations under the License.
|
14
|
-
"""Tests for pyglove.object_utils.thread_local."""
|
15
|
-
|
16
14
|
import threading
|
17
15
|
import time
|
18
16
|
import unittest
|
19
17
|
|
20
|
-
from pyglove.core.
|
18
|
+
from pyglove.core.utils import thread_local
|
21
19
|
|
22
20
|
|
23
21
|
class ThreadLocalTest(unittest.TestCase):
|
@@ -18,9 +18,9 @@ import dataclasses
|
|
18
18
|
import time
|
19
19
|
from typing import Any, Dict, List, Optional
|
20
20
|
|
21
|
-
from pyglove.core.
|
22
|
-
from pyglove.core.
|
23
|
-
from pyglove.core.
|
21
|
+
from pyglove.core.utils import error_utils
|
22
|
+
from pyglove.core.utils import json_conversion
|
23
|
+
from pyglove.core.utils import thread_local
|
24
24
|
|
25
25
|
|
26
26
|
class TimeIt:
|
@@ -56,6 +56,16 @@ class TimeIt:
|
|
56
56
|
**kwargs,
|
57
57
|
)
|
58
58
|
|
59
|
+
def merge(self, other: 'TimeIt.Status') -> 'TimeIt.Status':
|
60
|
+
"""Merges the status of two `pg.timeit`."""
|
61
|
+
assert other.name == self.name, (self.name, other.name)
|
62
|
+
return TimeIt.Status(
|
63
|
+
name=self.name,
|
64
|
+
elapse=self.elapse + other.elapse,
|
65
|
+
has_ended=self.has_ended and other.has_ended,
|
66
|
+
error=self.error or other.error,
|
67
|
+
)
|
68
|
+
|
59
69
|
@dataclasses.dataclass
|
60
70
|
class StatusSummary(json_conversion.JSONConvertible):
|
61
71
|
"""Aggregated summary for repeated calls for `pg.timeit`."""
|
@@ -125,7 +135,7 @@ class TimeIt:
|
|
125
135
|
self._name: str = name
|
126
136
|
self._start_time: Optional[float] = None
|
127
137
|
self._end_time: Optional[float] = None
|
128
|
-
self._child_contexts:
|
138
|
+
self._child_contexts: List[TimeIt] = []
|
129
139
|
self._error: Optional[error_utils.ErrorInfo] = None
|
130
140
|
self._parent: Optional[TimeIt] = None
|
131
141
|
|
@@ -137,13 +147,11 @@ class TimeIt:
|
|
137
147
|
@property
|
138
148
|
def children(self) -> List['TimeIt']:
|
139
149
|
"""Returns child contexts."""
|
140
|
-
return
|
150
|
+
return self._child_contexts
|
141
151
|
|
142
152
|
def add(self, context: 'TimeIt'):
|
143
153
|
"""Adds a child context."""
|
144
|
-
|
145
|
-
raise ValueError(f'`timeit` with name {context.name!r} already exists.')
|
146
|
-
self._child_contexts[context.name] = context
|
154
|
+
self._child_contexts.append(context)
|
147
155
|
|
148
156
|
def start(self):
|
149
157
|
"""Starts timing."""
|
@@ -206,11 +214,14 @@ class TimeIt:
|
|
206
214
|
has_ended=self.has_ended, error=self._error,
|
207
215
|
)
|
208
216
|
}
|
209
|
-
for child in self._child_contexts
|
217
|
+
for child in self._child_contexts:
|
210
218
|
child_result = child.status()
|
211
219
|
for k, v in child_result.items():
|
212
220
|
key = f'{self.name}.{k}' if self.name else k
|
213
|
-
|
221
|
+
if key in result:
|
222
|
+
result[key] = result[key].merge(v)
|
223
|
+
else:
|
224
|
+
result[key] = v
|
214
225
|
return result
|
215
226
|
|
216
227
|
def __enter__(self):
|
@@ -11,12 +11,11 @@
|
|
11
11
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
12
|
# See the License for the specific language governing permissions and
|
13
13
|
# limitations under the License.
|
14
|
-
|
15
14
|
import time
|
16
15
|
import unittest
|
17
16
|
|
18
|
-
from pyglove.core.
|
19
|
-
from pyglove.core.
|
17
|
+
from pyglove.core.utils import json_conversion
|
18
|
+
from pyglove.core.utils import timing
|
20
19
|
|
21
20
|
|
22
21
|
class TimeItTest(unittest.TestCase):
|
@@ -63,9 +62,9 @@ class TimeItTest(unittest.TestCase):
|
|
63
62
|
self.assertFalse(r['node.child'].has_ended)
|
64
63
|
self.assertTrue(r['node.child.grandchild'].has_ended)
|
65
64
|
|
66
|
-
with
|
67
|
-
|
68
|
-
|
65
|
+
with timing.timeit('grandchild') as t3:
|
66
|
+
time.sleep(0.5)
|
67
|
+
self.assertEqual(t1.children, [t2, t3])
|
69
68
|
|
70
69
|
elapse2 = t.elapse
|
71
70
|
self.assertTrue(t.has_ended)
|
@@ -73,17 +72,20 @@ class TimeItTest(unittest.TestCase):
|
|
73
72
|
time.sleep(0.5)
|
74
73
|
self.assertEqual(elapse2, t.elapse)
|
75
74
|
|
76
|
-
|
75
|
+
status = t.status()
|
77
76
|
self.assertEqual(
|
78
|
-
list(
|
77
|
+
list(status.keys()),
|
79
78
|
['node', 'node.child', 'node.child.grandchild']
|
80
79
|
)
|
81
80
|
self.assertEqual(
|
82
|
-
|
83
|
-
|
81
|
+
status['node.child.grandchild'].elapse, t2.elapse + t3.elapse
|
82
|
+
)
|
83
|
+
self.assertEqual(
|
84
|
+
sorted([v.elapse for v in status.values()], reverse=True),
|
85
|
+
[v.elapse for v in status.values()],
|
84
86
|
)
|
85
|
-
self.assertTrue(all(v.has_ended for v in
|
86
|
-
self.assertFalse(any(v.has_error for v in
|
87
|
+
self.assertTrue(all(v.has_ended for v in status.values()))
|
88
|
+
self.assertFalse(any(v.has_error for v in status.values()))
|
87
89
|
status = t.status()
|
88
90
|
json_dict = json_conversion.to_json(status)
|
89
91
|
status2 = json_conversion.from_json(json_dict)
|
@@ -17,7 +17,7 @@ import abc
|
|
17
17
|
import copy as copy_lib
|
18
18
|
import operator
|
19
19
|
from typing import Any, Callable, Iterable, Iterator, List, Optional, Union
|
20
|
-
from pyglove.core.
|
20
|
+
from pyglove.core.utils import formatting
|
21
21
|
|
22
22
|
|
23
23
|
class KeyPath(formatting.Formattable):
|
@@ -822,7 +822,7 @@ class StrKey(metaclass=abc.ABCMeta):
|
|
822
822
|
|
823
823
|
Example::
|
824
824
|
|
825
|
-
class MyKey(pg.
|
825
|
+
class MyKey(pg.utils.StrKey):
|
826
826
|
|
827
827
|
def __init__(self, name):
|
828
828
|
self.name = name
|
@@ -11,11 +11,9 @@
|
|
11
11
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
12
|
# See the License for the specific language governing permissions and
|
13
13
|
# limitations under the License.
|
14
|
-
"""Tests for pyglove.object_utils.value_location."""
|
15
|
-
|
16
14
|
import unittest
|
17
|
-
from pyglove.core.
|
18
|
-
from pyglove.core.
|
15
|
+
from pyglove.core.utils import formatting
|
16
|
+
from pyglove.core.utils import value_location
|
19
17
|
|
20
18
|
|
21
19
|
KeyPath = value_location.KeyPath
|
pyglove/core/views/base.py
CHANGED
@@ -136,18 +136,18 @@ import types
|
|
136
136
|
from typing import Any, Callable, Dict, Iterator, Optional, Sequence, Set, Type, Union
|
137
137
|
|
138
138
|
from pyglove.core import io as pg_io
|
139
|
-
from pyglove.core import object_utils
|
140
139
|
from pyglove.core import typing as pg_typing
|
140
|
+
from pyglove.core import utils
|
141
141
|
|
142
142
|
|
143
143
|
# Type definition for the value filter function.
|
144
144
|
NodeFilter = Callable[
|
145
145
|
[
|
146
|
-
|
147
|
-
Any,
|
148
|
-
Any,
|
146
|
+
utils.KeyPath, # The path to the value.
|
147
|
+
Any, # Current value.
|
148
|
+
Any, # Parent value
|
149
149
|
],
|
150
|
-
bool
|
150
|
+
bool, # Whether to include the value.
|
151
151
|
]
|
152
152
|
|
153
153
|
|
@@ -157,7 +157,7 @@ _TLS_KEY_OPERAND_STACK_BY_METHOD = '__view_operand_stack__'
|
|
157
157
|
_TLS_KEY_VIEW_OPTIONS = '__view_options__'
|
158
158
|
|
159
159
|
|
160
|
-
class Content(
|
160
|
+
class Content(utils.Formattable, metaclass=abc.ABCMeta):
|
161
161
|
"""Content: A type of media to be displayed in a view.
|
162
162
|
|
163
163
|
For example, `pg.Html` is a `Content` type that represents HTML to be
|
@@ -171,7 +171,7 @@ class Content(object_utils.Formattable, metaclass=abc.ABCMeta):
|
|
171
171
|
None
|
172
172
|
]
|
173
173
|
|
174
|
-
class SharedParts(
|
174
|
+
class SharedParts(utils.Formattable):
|
175
175
|
"""A part of the content that should appear just once.
|
176
176
|
|
177
177
|
For example, `pg.Html.Styles` is a `SharedParts` type that represents
|
@@ -244,7 +244,7 @@ class Content(object_utils.Formattable, metaclass=abc.ABCMeta):
|
|
244
244
|
**kwargs
|
245
245
|
) -> str:
|
246
246
|
if compact:
|
247
|
-
return
|
247
|
+
return utils.kvlist_str(
|
248
248
|
[
|
249
249
|
('parts', self._parts, {}),
|
250
250
|
],
|
@@ -252,7 +252,7 @@ class Content(object_utils.Formattable, metaclass=abc.ABCMeta):
|
|
252
252
|
compact=compact,
|
253
253
|
verbose=verbose,
|
254
254
|
root_indent=root_indent,
|
255
|
-
bracket_type=
|
255
|
+
bracket_type=utils.BracketType.ROUND,
|
256
256
|
)
|
257
257
|
return self.content
|
258
258
|
|
@@ -363,17 +363,16 @@ class Content(object_utils.Formattable, metaclass=abc.ABCMeta):
|
|
363
363
|
"""Formats the Content object."""
|
364
364
|
del kwargs
|
365
365
|
if compact:
|
366
|
-
return
|
366
|
+
return utils.kvlist_str(
|
367
367
|
[
|
368
368
|
('content', self.content, ''),
|
369
|
-
]
|
370
|
-
|
371
|
-
],
|
369
|
+
]
|
370
|
+
+ [(k, v, None) for k, v in self._shared_parts.items()],
|
372
371
|
label=self.__class__.__name__,
|
373
372
|
compact=compact,
|
374
373
|
verbose=verbose,
|
375
374
|
root_indent=root_indent,
|
376
|
-
bracket_type=
|
375
|
+
bracket_type=utils.BracketType.ROUND,
|
377
376
|
)
|
378
377
|
return self.to_str(content_only=content_only)
|
379
378
|
|
@@ -427,9 +426,9 @@ def view(
|
|
427
426
|
value: Any,
|
428
427
|
*,
|
429
428
|
name: Optional[str] = None,
|
430
|
-
root_path: Optional[
|
429
|
+
root_path: Optional[utils.KeyPath] = None,
|
431
430
|
view_id: str = 'html-tree-view',
|
432
|
-
**kwargs
|
431
|
+
**kwargs,
|
433
432
|
) -> Content:
|
434
433
|
"""Views an object through generating content based on a specific view.
|
435
434
|
|
@@ -451,8 +450,7 @@ def view(
|
|
451
450
|
with view_options(**kwargs) as options:
|
452
451
|
view_object = View.create(view_id)
|
453
452
|
return view_object.render(
|
454
|
-
value, name=name, root_path=root_path or
|
455
|
-
**options
|
453
|
+
value, name=name, root_path=root_path or utils.KeyPath(), **options
|
456
454
|
)
|
457
455
|
|
458
456
|
|
@@ -471,14 +469,14 @@ def view_options(**kwargs) -> Iterator[Dict[str, Any]]:
|
|
471
469
|
Yields:
|
472
470
|
The merged keyword arguments.
|
473
471
|
"""
|
474
|
-
parent_options =
|
472
|
+
parent_options = utils.thread_local_peek(_TLS_KEY_VIEW_OPTIONS, {})
|
475
473
|
# Deep merge the two dict.
|
476
|
-
options =
|
477
|
-
|
474
|
+
options = utils.merge([parent_options, kwargs])
|
475
|
+
utils.thread_local_push(_TLS_KEY_VIEW_OPTIONS, options)
|
478
476
|
try:
|
479
477
|
yield options
|
480
478
|
finally:
|
481
|
-
|
479
|
+
utils.thread_local_pop(_TLS_KEY_VIEW_OPTIONS)
|
482
480
|
|
483
481
|
|
484
482
|
class View(metaclass=abc.ABCMeta):
|
@@ -697,8 +695,8 @@ class View(metaclass=abc.ABCMeta):
|
|
697
695
|
value: Any,
|
698
696
|
*,
|
699
697
|
name: Optional[str] = None,
|
700
|
-
root_path: Optional[
|
701
|
-
**kwargs
|
698
|
+
root_path: Optional[utils.KeyPath] = None,
|
699
|
+
**kwargs,
|
702
700
|
) -> Content:
|
703
701
|
"""Renders the input value.
|
704
702
|
|
@@ -789,20 +787,18 @@ class View(metaclass=abc.ABCMeta):
|
|
789
787
|
) -> Iterator[Any]:
|
790
788
|
"""Context manager for tracking the value being rendered."""
|
791
789
|
del self
|
792
|
-
rendering_stack =
|
790
|
+
rendering_stack = utils.thread_local_get(
|
793
791
|
_TLS_KEY_OPERAND_STACK_BY_METHOD, {}
|
794
792
|
)
|
795
793
|
callsite_value = rendering_stack.get(view_method, None)
|
796
794
|
rendering_stack[view_method] = value
|
797
|
-
|
798
|
-
_TLS_KEY_OPERAND_STACK_BY_METHOD, rendering_stack
|
799
|
-
)
|
795
|
+
utils.thread_local_set(_TLS_KEY_OPERAND_STACK_BY_METHOD, rendering_stack)
|
800
796
|
try:
|
801
797
|
yield callsite_value
|
802
798
|
finally:
|
803
799
|
if callsite_value is None:
|
804
800
|
rendering_stack.pop(view_method)
|
805
801
|
if not rendering_stack:
|
806
|
-
|
802
|
+
utils.thread_local_del(_TLS_KEY_OPERAND_STACK_BY_METHOD)
|
807
803
|
else:
|
808
804
|
rendering_stack[view_method] = callsite_value
|
pyglove/core/views/html/base.py
CHANGED
@@ -20,8 +20,8 @@ import inspect
|
|
20
20
|
import typing
|
21
21
|
from typing import Any, Callable, Dict, Iterable, List, Optional, Sequence, Union
|
22
22
|
|
23
|
-
from pyglove.core import object_utils
|
24
23
|
from pyglove.core import typing as pg_typing
|
24
|
+
from pyglove.core import utils
|
25
25
|
from pyglove.core.views import base
|
26
26
|
|
27
27
|
NestableStr = Union[
|
@@ -33,11 +33,11 @@ NestableStr = Union[
|
|
33
33
|
NodeFilter = base.NodeFilter
|
34
34
|
NodeColor = Callable[
|
35
35
|
[
|
36
|
-
|
37
|
-
Any,
|
38
|
-
Any,
|
36
|
+
utils.KeyPath, # The path to the value.
|
37
|
+
Any, # Current value.
|
38
|
+
Any, # Parent value
|
39
39
|
],
|
40
|
-
Optional[str]
|
40
|
+
Optional[str], # The color of the node.
|
41
41
|
]
|
42
42
|
|
43
43
|
|
@@ -352,7 +352,7 @@ class Html(base.Content):
|
|
352
352
|
cls,
|
353
353
|
s: WritableTypes,
|
354
354
|
javascript_str: bool = False
|
355
|
-
) ->
|
355
|
+
) -> Any:
|
356
356
|
"""Escapes an HTML writable object."""
|
357
357
|
if s is None:
|
358
358
|
return None
|
@@ -386,7 +386,7 @@ class Html(base.Content):
|
|
386
386
|
cls, nestable_str: NestableStr, separator: str = ' ', dedup: bool = True
|
387
387
|
) -> Optional[str]:
|
388
388
|
"""Concates the string nodes in a nestable object."""
|
389
|
-
flattened =
|
389
|
+
flattened = utils.flatten(nestable_str)
|
390
390
|
if isinstance(flattened, str):
|
391
391
|
return flattened
|
392
392
|
elif isinstance(flattened, dict):
|
@@ -456,8 +456,8 @@ class HtmlView(base.View):
|
|
456
456
|
value: Any,
|
457
457
|
*,
|
458
458
|
name: Optional[str] = None,
|
459
|
-
root_path: Optional[
|
460
|
-
**kwargs
|
459
|
+
root_path: Optional[utils.KeyPath] = None,
|
460
|
+
**kwargs,
|
461
461
|
) -> Html:
|
462
462
|
"""Renders the input value into an HTML object."""
|
463
463
|
# For customized HtmlConvertible objects, call their `to_html()` method.
|
@@ -473,8 +473,8 @@ class HtmlView(base.View):
|
|
473
473
|
value: Any,
|
474
474
|
*,
|
475
475
|
name: Optional[str] = None,
|
476
|
-
root_path: Optional[
|
477
|
-
**kwargs
|
476
|
+
root_path: Optional[utils.KeyPath] = None,
|
477
|
+
**kwargs,
|
478
478
|
) -> Html:
|
479
479
|
"""View's implementation of HTML rendering."""
|
480
480
|
|
@@ -483,9 +483,9 @@ def to_html(
|
|
483
483
|
value: Any,
|
484
484
|
*,
|
485
485
|
name: Optional[str] = None,
|
486
|
-
root_path: Optional[
|
486
|
+
root_path: Optional[utils.KeyPath] = None,
|
487
487
|
view_id: str = 'html-tree-view',
|
488
|
-
**kwargs
|
488
|
+
**kwargs,
|
489
489
|
) -> Html:
|
490
490
|
"""Returns the HTML representation of a value.
|
491
491
|
|
@@ -517,10 +517,10 @@ def to_html_str(
|
|
517
517
|
value: Any,
|
518
518
|
*,
|
519
519
|
name: Optional[str] = None,
|
520
|
-
root_path: Optional[
|
520
|
+
root_path: Optional[utils.KeyPath] = None,
|
521
521
|
view_id: str = 'html-tree-view',
|
522
522
|
content_only: bool = False,
|
523
|
-
**kwargs
|
523
|
+
**kwargs,
|
524
524
|
) -> str:
|
525
525
|
"""Returns a HTML str for a value.
|
526
526
|
|
@@ -545,4 +545,3 @@ def to_html_str(
|
|
545
545
|
view_id=view_id,
|
546
546
|
**kwargs
|
547
547
|
).to_str(content_only=content_only)
|
548
|
-
|
@@ -19,7 +19,7 @@ import inspect
|
|
19
19
|
import sys
|
20
20
|
from typing import Annotated, Any, Dict, Iterator, List, Optional, Union
|
21
21
|
|
22
|
-
from pyglove.core import
|
22
|
+
from pyglove.core import utils
|
23
23
|
from pyglove.core.symbolic import object as pg_object
|
24
24
|
from pyglove.core.views.html import base
|
25
25
|
|
@@ -62,6 +62,7 @@ class HtmlControl(pg_object.Object):
|
|
62
62
|
super()._on_bound()
|
63
63
|
self._rendered = False
|
64
64
|
self._css_styles = []
|
65
|
+
self._dynamic_injected_css = set()
|
65
66
|
self._scripts = []
|
66
67
|
|
67
68
|
def add_style(self, *css: str) -> 'HtmlControl':
|
@@ -76,6 +77,7 @@ class HtmlControl(pg_object.Object):
|
|
76
77
|
def to_html(self, **kwargs) -> Html:
|
77
78
|
"""Returns the HTML representation of the control."""
|
78
79
|
self._rendered = True
|
80
|
+
self._dynamic_injected_css = set()
|
79
81
|
html = self._to_html(**kwargs)
|
80
82
|
return html.add_style(*self._css_styles).add_script(*self._scripts)
|
81
83
|
|
@@ -87,22 +89,22 @@ class HtmlControl(pg_object.Object):
|
|
87
89
|
@contextlib.contextmanager
|
88
90
|
def track_scripts(cls) -> Iterator[List[str]]:
|
89
91
|
del cls
|
90
|
-
all_tracked =
|
92
|
+
all_tracked = utils.thread_local_get(_TLS_TRACKED_SCRIPTS, [])
|
91
93
|
current = []
|
92
94
|
all_tracked.append(current)
|
93
|
-
|
95
|
+
utils.thread_local_set(_TLS_TRACKED_SCRIPTS, all_tracked)
|
94
96
|
try:
|
95
97
|
yield current
|
96
98
|
finally:
|
97
99
|
all_tracked.pop(-1)
|
98
100
|
if not all_tracked:
|
99
|
-
|
101
|
+
utils.thread_local_del(_TLS_TRACKED_SCRIPTS)
|
100
102
|
|
101
103
|
def _sync_members(self, **fields) -> None:
|
102
104
|
"""Synchronizes displayed values to members."""
|
103
105
|
self.rebind(fields, skip_notification=True, raise_on_no_change=False)
|
104
106
|
|
105
|
-
def _run_javascript(self, code: str) -> None:
|
107
|
+
def _run_javascript(self, code: str, debug: bool = False) -> None:
|
106
108
|
"""Runs the given JavaScript code."""
|
107
109
|
if not self.interactive:
|
108
110
|
raise ValueError(
|
@@ -113,14 +115,50 @@ class HtmlControl(pg_object.Object):
|
|
113
115
|
return
|
114
116
|
|
115
117
|
code = inspect.cleandoc(code)
|
118
|
+
if debug:
|
119
|
+
print('RUN JAVSCRIPT:\n', code)
|
116
120
|
if _notebook is not None:
|
117
121
|
_notebook.display(_notebook.Javascript(code))
|
118
122
|
|
119
123
|
# Track script execution.
|
120
|
-
all_tracked =
|
124
|
+
all_tracked = utils.thread_local_get(_TLS_TRACKED_SCRIPTS, [])
|
121
125
|
for tracked in all_tracked:
|
122
126
|
tracked.append(code)
|
123
127
|
|
128
|
+
def _add_css_rules(self, css: str) -> None:
|
129
|
+
if not self._rendered or not css or css in self._dynamic_injected_css:
|
130
|
+
return
|
131
|
+
self._run_javascript(
|
132
|
+
f"""
|
133
|
+
const style = document.createElement('style');
|
134
|
+
style.type = 'text/css';
|
135
|
+
style.textContent = "{Html.escape(css, javascript_str=True)}";
|
136
|
+
document.head.appendChild(style);
|
137
|
+
"""
|
138
|
+
)
|
139
|
+
self._dynamic_injected_css.add(css)
|
140
|
+
|
141
|
+
def _apply_css_rules(self, html: Html) -> None:
|
142
|
+
self._add_css_rules(html.styles.content)
|
143
|
+
|
144
|
+
def _insert_adjacent_html(
|
145
|
+
self,
|
146
|
+
element_selector_js: str,
|
147
|
+
html: Html,
|
148
|
+
var_name: str = 'elem',
|
149
|
+
position: str = 'beforeend'
|
150
|
+
):
|
151
|
+
self._run_javascript(
|
152
|
+
f"""
|
153
|
+
{element_selector_js}
|
154
|
+
{var_name}.insertAdjacentHTML(
|
155
|
+
"{position}",
|
156
|
+
"{Html.escape(html, javascript_str=True).to_str(content_only=True)}"
|
157
|
+
);
|
158
|
+
""",
|
159
|
+
)
|
160
|
+
self._apply_css_rules(html)
|
161
|
+
|
124
162
|
def element_id(self, child: Optional[str] = None) -> Optional[str]:
|
125
163
|
"""Returns the element id of this control or a child."""
|
126
164
|
if self.id is not None:
|
@@ -163,14 +201,13 @@ class HtmlControl(pg_object.Object):
|
|
163
201
|
child: Optional[str] = None,
|
164
202
|
) -> base.Html:
|
165
203
|
"""Updates the inner HTML of the control."""
|
166
|
-
js_html = Html.escape(html, javascript_str=True)
|
167
|
-
assert isinstance(js_html, Html), js_html
|
168
204
|
self._run_javascript(
|
169
205
|
f"""
|
170
206
|
elem = document.getElementById("{self.element_id(child)}");
|
171
|
-
elem.innerHTML = "{
|
207
|
+
elem.innerHTML = "{Html.escape(html, javascript_str=True).to_str(content_only=True)}";
|
172
208
|
"""
|
173
209
|
)
|
210
|
+
self._add_css_rules(html.styles.content)
|
174
211
|
return html
|
175
212
|
|
176
213
|
def _update_style(
|
@@ -74,7 +74,7 @@ class Label(HtmlControl):
|
|
74
74
|
|
75
75
|
def _to_html(self, **kwargs) -> Html:
|
76
76
|
text_elem = Html.element(
|
77
|
-
'a',
|
77
|
+
'a' if self.link is not None else 'span',
|
78
78
|
[self.text],
|
79
79
|
id=self.element_id(),
|
80
80
|
href=self.link,
|
@@ -96,6 +96,8 @@ class Label(HtmlControl):
|
|
96
96
|
tooltip: Union[str, Html, None] = None,
|
97
97
|
link: Optional[str] = None,
|
98
98
|
styles: Optional[Dict[str, Any]] = None,
|
99
|
+
add_class: Optional[List[str]] = None,
|
100
|
+
remove_class: Optional[List[str]] = None,
|
99
101
|
) -> None:
|
100
102
|
if text is not None:
|
101
103
|
self._sync_members(text=self._update_content(text))
|
@@ -105,7 +107,16 @@ class Label(HtmlControl):
|
|
105
107
|
self._sync_members(link=self._update_property('href', link))
|
106
108
|
if tooltip is not None:
|
107
109
|
self.tooltip.update(content=tooltip)
|
108
|
-
|
110
|
+
if add_class or remove_class:
|
111
|
+
css_classes = list(self.css_classes)
|
112
|
+
for x in add_class or []:
|
113
|
+
self._add_css_class(x)
|
114
|
+
css_classes.append(x)
|
115
|
+
for x in remove_class or []:
|
116
|
+
self._remove_css_class(x)
|
117
|
+
if x in css_classes:
|
118
|
+
css_classes.remove(x)
|
119
|
+
self._sync_members(css_classes=css_classes)
|
109
120
|
|
110
121
|
# Register converter for automatic conversion.
|
111
122
|
pg_typing.register_converter(str, Label, Label)
|