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.
Files changed (118) hide show
  1. pyglove/core/__init__.py +40 -21
  2. pyglove/core/coding/__init__.py +42 -0
  3. pyglove/core/coding/errors.py +111 -0
  4. pyglove/core/coding/errors_test.py +98 -0
  5. pyglove/core/coding/execution.py +312 -0
  6. pyglove/core/coding/execution_test.py +333 -0
  7. pyglove/core/{object_utils/codegen.py → coding/function_generation.py} +10 -4
  8. pyglove/core/{object_utils/codegen_test.py → coding/function_generation_test.py} +5 -7
  9. pyglove/core/coding/parsing.py +153 -0
  10. pyglove/core/coding/parsing_test.py +150 -0
  11. pyglove/core/coding/permissions.py +100 -0
  12. pyglove/core/coding/permissions_test.py +93 -0
  13. pyglove/core/geno/base.py +53 -38
  14. pyglove/core/geno/base_test.py +2 -4
  15. pyglove/core/geno/categorical.py +36 -27
  16. pyglove/core/geno/custom.py +18 -15
  17. pyglove/core/geno/numerical.py +19 -16
  18. pyglove/core/geno/space.py +3 -4
  19. pyglove/core/hyper/base.py +6 -6
  20. pyglove/core/hyper/categorical.py +91 -52
  21. pyglove/core/hyper/custom.py +7 -7
  22. pyglove/core/hyper/custom_test.py +9 -10
  23. pyglove/core/hyper/derived.py +30 -22
  24. pyglove/core/hyper/derived_test.py +3 -5
  25. pyglove/core/hyper/dynamic_evaluation.py +3 -4
  26. pyglove/core/hyper/evolvable.py +57 -46
  27. pyglove/core/hyper/numerical.py +48 -24
  28. pyglove/core/hyper/numerical_test.py +9 -9
  29. pyglove/core/hyper/object_template.py +58 -46
  30. pyglove/core/logging_test.py +0 -2
  31. pyglove/core/patching/object_factory.py +4 -4
  32. pyglove/core/patching/pattern_based.py +4 -4
  33. pyglove/core/patching/rule_based.py +4 -3
  34. pyglove/core/symbolic/__init__.py +4 -0
  35. pyglove/core/symbolic/base.py +200 -136
  36. pyglove/core/symbolic/base_test.py +17 -19
  37. pyglove/core/symbolic/boilerplate.py +4 -5
  38. pyglove/core/symbolic/class_wrapper.py +10 -14
  39. pyglove/core/symbolic/class_wrapper_test.py +2 -2
  40. pyglove/core/symbolic/compounding.py +2 -2
  41. pyglove/core/symbolic/compounding_test.py +2 -4
  42. pyglove/core/symbolic/contextual_object.py +288 -0
  43. pyglove/core/symbolic/contextual_object_test.py +327 -0
  44. pyglove/core/symbolic/dict.py +115 -87
  45. pyglove/core/symbolic/dict_test.py +188 -131
  46. pyglove/core/symbolic/diff.py +12 -12
  47. pyglove/core/symbolic/flags.py +1 -1
  48. pyglove/core/symbolic/functor.py +16 -15
  49. pyglove/core/symbolic/functor_test.py +2 -4
  50. pyglove/core/symbolic/inferred.py +2 -2
  51. pyglove/core/symbolic/list.py +70 -47
  52. pyglove/core/symbolic/list_test.py +117 -98
  53. pyglove/core/symbolic/object.py +59 -58
  54. pyglove/core/symbolic/object_test.py +143 -90
  55. pyglove/core/symbolic/origin.py +5 -7
  56. pyglove/core/symbolic/pure_symbolic.py +4 -3
  57. pyglove/core/symbolic/ref.py +33 -16
  58. pyglove/core/symbolic/ref_test.py +17 -0
  59. pyglove/core/tuning/local_backend.py +2 -2
  60. pyglove/core/tuning/protocols.py +3 -3
  61. pyglove/core/typing/annotation_conversion.py +8 -3
  62. pyglove/core/typing/annotation_conversion_test.py +8 -0
  63. pyglove/core/typing/callable_ext.py +11 -13
  64. pyglove/core/typing/callable_signature.py +22 -19
  65. pyglove/core/typing/callable_signature_test.py +3 -5
  66. pyglove/core/typing/class_schema.py +93 -54
  67. pyglove/core/typing/class_schema_test.py +4 -5
  68. pyglove/core/typing/custom_typing.py +5 -4
  69. pyglove/core/typing/key_specs.py +5 -7
  70. pyglove/core/typing/key_specs_test.py +4 -4
  71. pyglove/core/typing/type_conversion.py +4 -5
  72. pyglove/core/typing/type_conversion_test.py +12 -12
  73. pyglove/core/typing/typed_missing.py +6 -7
  74. pyglove/core/typing/typed_missing_test.py +7 -8
  75. pyglove/core/typing/value_specs.py +287 -144
  76. pyglove/core/typing/value_specs_test.py +148 -25
  77. pyglove/core/utils/__init__.py +172 -0
  78. pyglove/core/{object_utils → utils}/common_traits.py +2 -2
  79. pyglove/core/{object_utils → utils}/common_traits_test.py +1 -3
  80. pyglove/core/utils/contextual.py +147 -0
  81. pyglove/core/utils/contextual_test.py +88 -0
  82. pyglove/core/{object_utils → utils}/docstr_utils_test.py +1 -3
  83. pyglove/core/{object_utils → utils}/error_utils.py +3 -3
  84. pyglove/core/{object_utils → utils}/error_utils_test.py +1 -1
  85. pyglove/core/{object_utils → utils}/formatting.py +1 -1
  86. pyglove/core/{object_utils → utils}/formatting_test.py +1 -2
  87. pyglove/core/{object_utils → utils}/hierarchical.py +23 -25
  88. pyglove/core/{object_utils → utils}/hierarchical_test.py +3 -5
  89. pyglove/core/{object_utils → utils}/json_conversion.py +1 -1
  90. pyglove/core/{object_utils → utils}/json_conversion_test.py +1 -3
  91. pyglove/core/{object_utils → utils}/missing.py +2 -2
  92. pyglove/core/{object_utils → utils}/missing_test.py +2 -4
  93. pyglove/core/utils/text_color.py +128 -0
  94. pyglove/core/utils/text_color_test.py +94 -0
  95. pyglove/core/{object_utils → utils}/thread_local_test.py +1 -3
  96. pyglove/core/{object_utils → utils}/timing.py +21 -10
  97. pyglove/core/{object_utils → utils}/timing_test.py +14 -12
  98. pyglove/core/{object_utils → utils}/value_location.py +2 -2
  99. pyglove/core/{object_utils → utils}/value_location_test.py +2 -4
  100. pyglove/core/views/base.py +25 -29
  101. pyglove/core/views/html/base.py +15 -16
  102. pyglove/core/views/html/controls/base.py +46 -9
  103. pyglove/core/views/html/controls/label.py +13 -2
  104. pyglove/core/views/html/controls/label_test.py +27 -8
  105. pyglove/core/views/html/controls/progress_bar.py +3 -5
  106. pyglove/core/views/html/controls/progress_bar_test.py +2 -2
  107. pyglove/core/views/html/controls/tab.py +217 -66
  108. pyglove/core/views/html/controls/tab_test.py +46 -15
  109. pyglove/core/views/html/tree_view.py +39 -37
  110. {pyglove-0.4.5.dev202411132359.dist-info → pyglove-0.4.5.dev202501250807.dist-info}/METADATA +17 -3
  111. pyglove-0.4.5.dev202501250807.dist-info/RECORD +218 -0
  112. {pyglove-0.4.5.dev202411132359.dist-info → pyglove-0.4.5.dev202501250807.dist-info}/WHEEL +1 -1
  113. pyglove/core/object_utils/__init__.py +0 -164
  114. pyglove-0.4.5.dev202411132359.dist-info/RECORD +0 -203
  115. /pyglove/core/{object_utils → utils}/docstr_utils.py +0 -0
  116. /pyglove/core/{object_utils → utils}/thread_local.py +0 -0
  117. {pyglove-0.4.5.dev202411132359.dist-info → pyglove-0.4.5.dev202501250807.dist-info}/LICENSE +0 -0
  118. {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.object_utils import thread_local
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.object_utils import error_utils
22
- from pyglove.core.object_utils import json_conversion
23
- from pyglove.core.object_utils import thread_local
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: Dict[str, TimeIt] = {}
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 list(self._child_contexts.values())
150
+ return self._child_contexts
141
151
 
142
152
  def add(self, context: 'TimeIt'):
143
153
  """Adds a child context."""
144
- if context.name in self._child_contexts:
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.values():
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
- result[key] = v
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.object_utils import json_conversion
19
- from pyglove.core.object_utils import timing
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 self.assertRaisesRegex(ValueError, '.* already exists'):
67
- with timing.timeit('grandchild'):
68
- pass
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
- statuss = t.status()
75
+ status = t.status()
77
76
  self.assertEqual(
78
- list(statuss.keys()),
77
+ list(status.keys()),
79
78
  ['node', 'node.child', 'node.child.grandchild']
80
79
  )
81
80
  self.assertEqual(
82
- sorted([v.elapse for v in statuss.values()], reverse=True),
83
- [v.elapse for v in statuss.values()],
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 statuss.values()))
86
- self.assertFalse(any(v.has_error for v in statuss.values()))
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.object_utils import formatting
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.object_utils.StrKey):
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.object_utils import formatting
18
- from pyglove.core.object_utils import value_location
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
@@ -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
- object_utils.KeyPath, # The path to the value.
147
- Any, # Current value.
148
- Any, # Parent value
146
+ utils.KeyPath, # The path to the value.
147
+ Any, # Current value.
148
+ Any, # Parent value
149
149
  ],
150
- bool # Whether to include the value.
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(object_utils.Formattable, metaclass=abc.ABCMeta):
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(object_utils.Formattable):
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 object_utils.kvlist_str(
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=object_utils.BracketType.ROUND,
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 object_utils.kvlist_str(
366
+ return utils.kvlist_str(
367
367
  [
368
368
  ('content', self.content, ''),
369
- ] + [
370
- (k, v, None) for k, v in self._shared_parts.items()
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=object_utils.BracketType.ROUND,
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[object_utils.KeyPath] = None,
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 object_utils.KeyPath(),
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 = object_utils.thread_local_peek(_TLS_KEY_VIEW_OPTIONS, {})
472
+ parent_options = utils.thread_local_peek(_TLS_KEY_VIEW_OPTIONS, {})
475
473
  # Deep merge the two dict.
476
- options = object_utils.merge([parent_options, kwargs])
477
- object_utils.thread_local_push(_TLS_KEY_VIEW_OPTIONS, options)
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
- object_utils.thread_local_pop(_TLS_KEY_VIEW_OPTIONS)
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[object_utils.KeyPath] = None,
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 = object_utils.thread_local_get(
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
- object_utils.thread_local_set(
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
- object_utils.thread_local_del(_TLS_KEY_OPERAND_STACK_BY_METHOD)
802
+ utils.thread_local_del(_TLS_KEY_OPERAND_STACK_BY_METHOD)
807
803
  else:
808
804
  rendering_stack[view_method] = callsite_value
@@ -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
- object_utils.KeyPath, # The path to the value.
37
- Any, # Current value.
38
- Any, # Parent value
36
+ utils.KeyPath, # The path to the value.
37
+ Any, # Current value.
38
+ Any, # Parent value
39
39
  ],
40
- Optional[str] # The color of the node.
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
- ) -> Union[str, 'Html', None]:
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 = object_utils.flatten(nestable_str)
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[object_utils.KeyPath] = None,
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[object_utils.KeyPath] = None,
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[object_utils.KeyPath] = None,
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[object_utils.KeyPath] = None,
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 object_utils
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 = object_utils.thread_local_get(_TLS_TRACKED_SCRIPTS, [])
92
+ all_tracked = utils.thread_local_get(_TLS_TRACKED_SCRIPTS, [])
91
93
  current = []
92
94
  all_tracked.append(current)
93
- object_utils.thread_local_set(_TLS_TRACKED_SCRIPTS, all_tracked)
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
- object_utils.thread_local_del(_TLS_TRACKED_SCRIPTS)
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 = object_utils.thread_local_get(_TLS_TRACKED_SCRIPTS, [])
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 = "{js_html.to_str(content_only=True)}";
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)