pyglove 0.4.5.dev20240319__py3-none-any.whl → 0.4.5.dev202501140808__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 (145) hide show
  1. pyglove/core/__init__.py +54 -20
  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 +309 -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 +54 -41
  14. pyglove/core/geno/base_test.py +2 -4
  15. pyglove/core/geno/categorical.py +37 -28
  16. pyglove/core/geno/custom.py +19 -16
  17. pyglove/core/geno/numerical.py +20 -17
  18. pyglove/core/geno/space.py +4 -5
  19. pyglove/core/hyper/base.py +6 -6
  20. pyglove/core/hyper/categorical.py +94 -55
  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 +2 -4
  25. pyglove/core/hyper/dynamic_evaluation.py +5 -6
  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/io/__init__.py +1 -0
  31. pyglove/core/io/file_system.py +17 -7
  32. pyglove/core/io/file_system_test.py +2 -0
  33. pyglove/core/io/sequence.py +299 -0
  34. pyglove/core/io/sequence_test.py +124 -0
  35. pyglove/core/logging_test.py +0 -2
  36. pyglove/core/patching/object_factory.py +4 -4
  37. pyglove/core/patching/pattern_based.py +4 -4
  38. pyglove/core/patching/rule_based.py +17 -5
  39. pyglove/core/patching/rule_based_test.py +27 -4
  40. pyglove/core/symbolic/__init__.py +2 -7
  41. pyglove/core/symbolic/base.py +320 -183
  42. pyglove/core/symbolic/base_test.py +123 -19
  43. pyglove/core/symbolic/boilerplate.py +7 -13
  44. pyglove/core/symbolic/boilerplate_test.py +25 -23
  45. pyglove/core/symbolic/class_wrapper.py +48 -45
  46. pyglove/core/symbolic/class_wrapper_test.py +2 -2
  47. pyglove/core/symbolic/compounding.py +9 -15
  48. pyglove/core/symbolic/compounding_test.py +2 -4
  49. pyglove/core/symbolic/dict.py +154 -110
  50. pyglove/core/symbolic/dict_test.py +238 -130
  51. pyglove/core/symbolic/diff.py +199 -10
  52. pyglove/core/symbolic/diff_test.py +226 -0
  53. pyglove/core/symbolic/flags.py +1 -1
  54. pyglove/core/symbolic/functor.py +29 -26
  55. pyglove/core/symbolic/functor_test.py +102 -50
  56. pyglove/core/symbolic/inferred.py +2 -2
  57. pyglove/core/symbolic/list.py +81 -50
  58. pyglove/core/symbolic/list_test.py +119 -97
  59. pyglove/core/symbolic/object.py +225 -113
  60. pyglove/core/symbolic/object_test.py +320 -108
  61. pyglove/core/symbolic/origin.py +17 -14
  62. pyglove/core/symbolic/origin_test.py +4 -2
  63. pyglove/core/symbolic/pure_symbolic.py +4 -3
  64. pyglove/core/symbolic/ref.py +108 -21
  65. pyglove/core/symbolic/ref_test.py +93 -0
  66. pyglove/core/symbolic/symbolize_test.py +10 -2
  67. pyglove/core/tuning/local_backend.py +2 -2
  68. pyglove/core/tuning/protocols.py +3 -3
  69. pyglove/core/tuning/sample_test.py +3 -3
  70. pyglove/core/typing/__init__.py +14 -5
  71. pyglove/core/typing/annotation_conversion.py +43 -27
  72. pyglove/core/typing/annotation_conversion_test.py +23 -0
  73. pyglove/core/typing/callable_ext.py +241 -3
  74. pyglove/core/typing/callable_ext_test.py +255 -0
  75. pyglove/core/typing/callable_signature.py +510 -66
  76. pyglove/core/typing/callable_signature_test.py +619 -99
  77. pyglove/core/typing/class_schema.py +229 -154
  78. pyglove/core/typing/class_schema_test.py +149 -95
  79. pyglove/core/typing/custom_typing.py +5 -4
  80. pyglove/core/typing/inspect.py +63 -0
  81. pyglove/core/typing/inspect_test.py +39 -0
  82. pyglove/core/typing/key_specs.py +10 -11
  83. pyglove/core/typing/key_specs_test.py +7 -4
  84. pyglove/core/typing/type_conversion.py +4 -5
  85. pyglove/core/typing/type_conversion_test.py +12 -12
  86. pyglove/core/typing/typed_missing.py +6 -7
  87. pyglove/core/typing/typed_missing_test.py +7 -8
  88. pyglove/core/typing/value_specs.py +604 -362
  89. pyglove/core/typing/value_specs_test.py +328 -90
  90. pyglove/core/utils/__init__.py +164 -0
  91. pyglove/core/{object_utils → utils}/common_traits.py +3 -67
  92. pyglove/core/utils/common_traits_test.py +36 -0
  93. pyglove/core/{object_utils → utils}/docstr_utils.py +23 -0
  94. pyglove/core/{object_utils → utils}/docstr_utils_test.py +36 -4
  95. pyglove/core/{object_utils → utils}/error_utils.py +78 -9
  96. pyglove/core/{object_utils → utils}/error_utils_test.py +61 -5
  97. pyglove/core/utils/formatting.py +464 -0
  98. pyglove/core/utils/formatting_test.py +453 -0
  99. pyglove/core/{object_utils → utils}/hierarchical.py +23 -25
  100. pyglove/core/{object_utils → utils}/hierarchical_test.py +3 -5
  101. pyglove/core/{object_utils → utils}/json_conversion.py +177 -52
  102. pyglove/core/{object_utils → utils}/json_conversion_test.py +97 -16
  103. pyglove/core/{object_utils → utils}/missing.py +3 -3
  104. pyglove/core/{object_utils → utils}/missing_test.py +2 -4
  105. pyglove/core/utils/text_color.py +128 -0
  106. pyglove/core/utils/text_color_test.py +94 -0
  107. pyglove/core/{object_utils → utils}/thread_local_test.py +1 -3
  108. pyglove/core/utils/timing.py +236 -0
  109. pyglove/core/utils/timing_test.py +154 -0
  110. pyglove/core/{object_utils → utils}/value_location.py +275 -6
  111. pyglove/core/utils/value_location_test.py +707 -0
  112. pyglove/core/views/__init__.py +32 -0
  113. pyglove/core/views/base.py +804 -0
  114. pyglove/core/views/base_test.py +580 -0
  115. pyglove/core/views/html/__init__.py +27 -0
  116. pyglove/core/views/html/base.py +547 -0
  117. pyglove/core/views/html/base_test.py +830 -0
  118. pyglove/core/views/html/controls/__init__.py +35 -0
  119. pyglove/core/views/html/controls/base.py +275 -0
  120. pyglove/core/views/html/controls/label.py +207 -0
  121. pyglove/core/views/html/controls/label_test.py +157 -0
  122. pyglove/core/views/html/controls/progress_bar.py +183 -0
  123. pyglove/core/views/html/controls/progress_bar_test.py +97 -0
  124. pyglove/core/views/html/controls/tab.py +320 -0
  125. pyglove/core/views/html/controls/tab_test.py +87 -0
  126. pyglove/core/views/html/controls/tooltip.py +99 -0
  127. pyglove/core/views/html/controls/tooltip_test.py +99 -0
  128. pyglove/core/views/html/tree_view.py +1517 -0
  129. pyglove/core/views/html/tree_view_test.py +1461 -0
  130. {pyglove-0.4.5.dev20240319.dist-info → pyglove-0.4.5.dev202501140808.dist-info}/METADATA +18 -4
  131. pyglove-0.4.5.dev202501140808.dist-info/RECORD +214 -0
  132. {pyglove-0.4.5.dev20240319.dist-info → pyglove-0.4.5.dev202501140808.dist-info}/WHEEL +1 -1
  133. pyglove/core/object_utils/__init__.py +0 -154
  134. pyglove/core/object_utils/common_traits_test.py +0 -82
  135. pyglove/core/object_utils/formatting.py +0 -234
  136. pyglove/core/object_utils/formatting_test.py +0 -223
  137. pyglove/core/object_utils/value_location_test.py +0 -385
  138. pyglove/core/symbolic/schema_utils.py +0 -327
  139. pyglove/core/symbolic/schema_utils_test.py +0 -57
  140. pyglove/core/typing/class_schema_utils.py +0 -202
  141. pyglove/core/typing/class_schema_utils_test.py +0 -194
  142. pyglove-0.4.5.dev20240319.dist-info/RECORD +0 -185
  143. /pyglove/core/{object_utils → utils}/thread_local.py +0 -0
  144. {pyglove-0.4.5.dev20240319.dist-info → pyglove-0.4.5.dev202501140808.dist-info}/LICENSE +0 -0
  145. {pyglove-0.4.5.dev20240319.dist-info → pyglove-0.4.5.dev202501140808.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,580 @@
1
+ # Copyright 2024 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
+
15
+ import copy
16
+ import functools
17
+ import os
18
+ import tempfile
19
+ from typing import Any, Callable, Iterable, Optional, Union
20
+ import unittest
21
+
22
+ from pyglove.core import io as pg_io
23
+ from pyglove.core.views import base
24
+
25
+ Content = base.Content
26
+ View = base.View
27
+
28
+
29
+ class ReferenceLinks(Content.SharedParts):
30
+
31
+ @functools.cached_property
32
+ def content(self) -> str:
33
+ return '\n'.join(
34
+ [
35
+ f'link={url}' for url in self.parts.keys()
36
+ ]
37
+ )
38
+
39
+
40
+ class SharedPartsTest(unittest.TestCase):
41
+
42
+ def test_basics(self):
43
+ links = ReferenceLinks()
44
+ self.assertFalse(links)
45
+ self.assertEqual(links.content, '')
46
+ self.assertEqual(list(links), [])
47
+
48
+ links = ReferenceLinks('https://x/y.css')
49
+ self.assertEqual(links.content, 'link=https://x/y.css')
50
+ self.assertTrue(links)
51
+ self.assertIn('https://x/y.css', links)
52
+ self.assertNotIn('https://x/z.css', links)
53
+ self.assertEqual(list(links), ['https://x/y.css'])
54
+
55
+ links = ReferenceLinks('https://x/y.css', 'https://x/z.css')
56
+ self.assertEqual(
57
+ links.content,
58
+ 'link=https://x/y.css\nlink=https://x/z.css'
59
+ )
60
+ self.assertTrue(links)
61
+ self.assertIn('https://x/y.css', links)
62
+ self.assertIn('https://x/z.css', links)
63
+ self.assertEqual(list(links), ['https://x/y.css', 'https://x/z.css'])
64
+
65
+ def test_add(self):
66
+ links = ReferenceLinks('https://x/y.css')
67
+ self.assertEqual(links.content, 'link=https://x/y.css')
68
+
69
+ # Updated.
70
+ self.assertTrue(links.add('https://x/z.css'))
71
+ self.assertEqual(links.parts, {'https://x/y.css': 1, 'https://x/z.css': 1})
72
+ # Make sure the cached property `content` is updated.
73
+ self.assertEqual(
74
+ links.content,
75
+ 'link=https://x/y.css\nlink=https://x/z.css'
76
+ )
77
+ # Not updated but the number of reference gets updated.
78
+ self.assertFalse(links.add('https://x/z.css'))
79
+ self.assertEqual(links.parts, {'https://x/y.css': 1, 'https://x/z.css': 2})
80
+
81
+ # Adding None is ignored.
82
+ self.assertFalse(links.add(None))
83
+ self.assertEqual(links.parts, {'https://x/y.css': 1, 'https://x/z.css': 2})
84
+
85
+ # Add another instance of `ReferenceLinks`.
86
+ self.assertTrue(
87
+ links.add(ReferenceLinks('https://x/y.css', 'https://x/w.css'))
88
+ )
89
+ self.assertEqual(
90
+ links.parts,
91
+ {
92
+ 'https://x/y.css': 2,
93
+ 'https://x/z.css': 2,
94
+ 'https://x/w.css': 1
95
+ }
96
+ )
97
+
98
+ def test_copy(self):
99
+ links = ReferenceLinks('https://x/y.css')
100
+ links2 = copy.copy(links)
101
+ self.assertEqual(links, links2)
102
+ self.assertEqual(links.parts, links2.parts)
103
+
104
+ def test_eq(self):
105
+ links1 = ReferenceLinks('https://x/y.css')
106
+ links2 = ReferenceLinks('https://x/y.css')
107
+ self.assertEqual(links1, links2)
108
+ self.assertNotEqual(links1, 'abc')
109
+ self.assertNotEqual(links1, ReferenceLinks('https://x/z.css'))
110
+
111
+ def test_format(self):
112
+ links = ReferenceLinks('https://x/y.css')
113
+ self.assertEqual(
114
+ repr(links),
115
+ """ReferenceLinks(parts={'https://x/y.css': 1})"""
116
+ )
117
+ self.assertEqual(
118
+ str(links),
119
+ """link=https://x/y.css"""
120
+ )
121
+
122
+
123
+ class Document(Content):
124
+
125
+ WritableTypes = Union[ # pylint: disable=invalid-name
126
+ str, 'Document', None, Callable[[], Union[str, 'Document', None]]
127
+ ]
128
+
129
+ def __init__( # pylint: disable=useless-super-delegation
130
+ self,
131
+ *content: WritableTypes, ref_links: Optional[Iterable[str]] = None):
132
+ super().__init__(*content, ref_links=ReferenceLinks(*(ref_links or [])))
133
+
134
+ @property
135
+ def ref_links(self) -> ReferenceLinks:
136
+ return self._shared_parts['ref_links']
137
+
138
+ def to_str(self, *, content_only: bool = False, **kwargs):
139
+ if content_only:
140
+ return self.content
141
+ return self.ref_links.content + '\n' + self.content
142
+
143
+
144
+ class ContentTest(unittest.TestCase):
145
+
146
+ def test_basics(self):
147
+ document = Document()
148
+ self.assertEqual(document.content, '')
149
+
150
+ document = Document('abc', 'def', ref_links=['https://x/y.css'])
151
+ self.assertEqual(document.content, 'abcdef')
152
+ self.assertEqual(document.to_str(content_only=True), 'abcdef')
153
+ self.assertEqual(document.to_str(), 'link=https://x/y.css\nabcdef')
154
+ self.assertEqual(document.shared_parts, {'ref_links': document.ref_links})
155
+ self.assertEqual(
156
+ Document('abc', ref_links=['https://x/z.css']),
157
+ Document('abc', ref_links=['https://x/z.css'])
158
+ )
159
+ self.assertNotEqual(
160
+ Document('abc', ref_links=['https://x/y.css']),
161
+ 'abc'
162
+ )
163
+ self.assertNotEqual(
164
+ Document('abc', ref_links=['https://x/y.css']),
165
+ Document('abc', ref_links=['https://x/z.css'])
166
+ )
167
+ self.assertEqual(
168
+ hash(Document('abc', ref_links=['https://x/y.css'])),
169
+ hash(Document('abc', ref_links=['https://x/y.css']))
170
+ )
171
+ self.assertNotEqual(
172
+ hash(Document('abc', ref_links=['https://x/y.css'])),
173
+ hash(Document('abc', ref_links=['https://x/z.css']))
174
+ )
175
+
176
+ def test_write(self):
177
+ document = Document('abc', ref_links=['https://x/y.css'])
178
+ self.assertEqual(document.content, 'abc')
179
+
180
+ self.assertIs(document.write('def'), document)
181
+ self.assertEqual(document.content, 'abcdef')
182
+
183
+ # No op.
184
+ document.write(None)
185
+ self.assertEqual(document.content, 'abcdef')
186
+
187
+ # Write another instance of `Document`.
188
+ document.write(
189
+ Document('ghi', ref_links=['https://x/y.css', 'https://x/z.css'])
190
+ )
191
+ self.assertEqual(document.content, 'abcdefghi')
192
+ self.assertEqual(
193
+ document.to_str(),
194
+ 'link=https://x/y.css\nlink=https://x/z.css\nabcdefghi'
195
+ )
196
+
197
+ # Write a lambda.
198
+ document.write(lambda: 'jkl')
199
+ self.assertEqual(document.content, 'abcdefghijkl')
200
+ self.assertEqual(
201
+ document.to_str(),
202
+ 'link=https://x/y.css\nlink=https://x/z.css\nabcdefghijkl'
203
+ )
204
+
205
+ document.write(lambda: None)
206
+ self.assertEqual(document.content, 'abcdefghijkl')
207
+
208
+ document.write(lambda: Document('mno'))
209
+ self.assertEqual(document.content, 'abcdefghijklmno')
210
+
211
+ def test_save(self):
212
+ filename = os.path.join(tempfile.gettempdir(), '1', 'test_doc.txt')
213
+ Document('abc', ref_links=['https://x/y.css']).save(filename)
214
+ self.assertTrue(pg_io.path_exists(filename))
215
+ self.assertEqual(pg_io.readfile(filename), 'link=https://x/y.css\nabc')
216
+
217
+ def test_add_and_radd(self):
218
+ self.assertEqual(
219
+ (Document('abc', ref_links=['https://x/y.css']) + 'def').to_str(),
220
+ 'link=https://x/y.css\nabcdef'
221
+ )
222
+ self.assertEqual(
223
+ ('def' + Document('abc', ref_links=['https://x/y.css'])).to_str(),
224
+ 'link=https://x/y.css\ndefabc'
225
+ )
226
+ self.assertEqual(
227
+ (Document('abc', ref_links=['https://x/y.css']) + None).to_str(),
228
+ 'link=https://x/y.css\nabc'
229
+ )
230
+ self.assertEqual(
231
+ (None + Document('abc', ref_links=['https://x/y.css'])).to_str(),
232
+ 'link=https://x/y.css\nabc'
233
+ )
234
+ self.assertEqual(
235
+ (Document('abc', ref_links=['https://x/y.css'])
236
+ + (lambda: 'def')).to_str(),
237
+ 'link=https://x/y.css\nabcdef'
238
+ )
239
+ self.assertEqual(
240
+ ((lambda: 'def')
241
+ + Document('abc', ref_links=['https://x/y.css'])).to_str(),
242
+ 'link=https://x/y.css\ndefabc'
243
+ )
244
+
245
+ def test_format(self):
246
+ document = Document('abc', ref_links=['https://x/y.css'])
247
+ self.assertEqual(
248
+ repr(document),
249
+ (
250
+ 'Document(content=\'abc\', '
251
+ 'ref_links=ReferenceLinks(parts={\'https://x/y.css\': 1}))'
252
+ )
253
+ )
254
+ self.assertEqual(
255
+ str(document),
256
+ 'link=https://x/y.css\nabc'
257
+ )
258
+
259
+ def test_from_value(self):
260
+ document = Document('abc', ref_links=['https://x/y.css'])
261
+ self.assertIs(Document.from_value(document), document)
262
+ document2 = Document.from_value(document, copy=True)
263
+ self.assertIsNot(document2, document)
264
+ self.assertEqual(document2, document)
265
+
266
+ self.assertIsNone(Document.from_value(None))
267
+ self.assertEqual(Document.from_value('abc'), Document('abc'))
268
+ self.assertEqual(
269
+ Document.from_value(lambda: 'abc'), Document('abc')
270
+ )
271
+
272
+
273
+ class TestView(View):
274
+
275
+ VIEW_ID = 'test'
276
+
277
+ class Extension(View.Extension):
278
+
279
+ def __init__(self, x: int):
280
+ super().__init__()
281
+ self.x = x
282
+
283
+ def __str__(self):
284
+ return f'{self.__class__.__name__}(x={self.x})'
285
+
286
+ def _test_view_render(
287
+ self,
288
+ *,
289
+ view: 'TestView',
290
+ use_summary: bool = False,
291
+ use_content: bool = False,
292
+ **kwargs):
293
+ if use_summary:
294
+ return view.summary(self, **kwargs)
295
+ if use_content:
296
+ return view.content(self, **kwargs)
297
+ return Document(f'[Custom Document] {self}')
298
+
299
+ def _test_view_summary(
300
+ self,
301
+ view: 'TestView',
302
+ default_behavior: bool = False,
303
+ **kwargs):
304
+ if default_behavior:
305
+ return view.summary(self, **kwargs)
306
+ return Document(f'[Custom Summary] {self}')
307
+
308
+ def _test_view_content(self, **kwargs):
309
+ del kwargs
310
+ return Document(f'[Custom Content] {self}')
311
+
312
+ def bar(self, x: int = 2):
313
+ return x
314
+
315
+ @View.extension_method('_test_view_render')
316
+ def render(
317
+ self,
318
+ value: Any,
319
+ *,
320
+ greeting: str = 'hi',
321
+ **kwargs):
322
+ return Document(f'{greeting} {value}')
323
+
324
+ @View.extension_method('_test_view_summary')
325
+ def summary(
326
+ self,
327
+ value: Any,
328
+ title: str = 'abc',
329
+ **kwargs
330
+ ):
331
+ del kwargs
332
+ return Document(f'[Default Summary] {title}: {value}')
333
+
334
+ @View.extension_method('_test_view_content')
335
+ def content(self, value: Any, **kwargs):
336
+ del kwargs
337
+ return Document(f'[Default Content] {value}')
338
+
339
+ def render_key(self, key: str, *, value: Optional[Any] = None, **kwargs):
340
+ del kwargs
341
+ return Document(f'[Default Key] {key}: {value}')
342
+
343
+ def foo(self, x: int = 1):
344
+ return x
345
+
346
+
347
+ class TestView2(View):
348
+
349
+ VIEW_ID = 'test2'
350
+
351
+ class Extension(View.Extension):
352
+
353
+ def _test_view2_render(self, value: Any, **kwargs):
354
+ del kwargs
355
+ return Document(f'<h2>{value}</h2>')
356
+
357
+ def _test_view2_tooltip(
358
+ self,
359
+ **kwargs,
360
+ ):
361
+ del kwargs
362
+ return Document('<h2>Tooltip</h2>')
363
+
364
+ @View.extension_method('_test_view2_render')
365
+ def render(self, value: Any, **kwargs):
366
+ return Document(f'<h1>{value}</h1>')
367
+
368
+ @View.extension_method('_test_view2_tooltip')
369
+ def tooltip(self, *, value: Optional[Any] = None, **kwargs):
370
+ del kwargs
371
+ return Document(f'<tooltip>{value}</tooltip>')
372
+
373
+
374
+ class MyObject(TestView.Extension, TestView2.Extension):
375
+
376
+ def __init__(self, x: int, y: str):
377
+ super().__init__(x)
378
+ self.y = y
379
+
380
+ def _test_view_content(
381
+ self, *, text: str = 'MyObject', **kwargs
382
+ ):
383
+ return Document(f'[{text}] {self.x} {self.y}')
384
+
385
+ def _test_view2_render(self, **kwargs):
386
+ return Document(f'<span>{self.x}</span>')
387
+
388
+
389
+ class MyObject2(MyObject):
390
+
391
+ def _doc_content(self, **kwargs):
392
+ del kwargs
393
+ return Document('Nothing')
394
+
395
+
396
+ class ViewTest(unittest.TestCase):
397
+
398
+ def test_bad_views(self):
399
+ # VIEW ID must be present for concrete View classes.
400
+ with self.assertRaisesRegex(
401
+ ValueError, '`VIEW_ID` must be set'
402
+ ):
403
+ class NoTypeNonAbstractView(View): # pylint: disable=unused-variable
404
+
405
+ def render(self, *args, **kwargs):
406
+ return Document('foo')
407
+
408
+ with self.assertRaisesRegex(
409
+ TypeError, 'must have a `value` argument'
410
+ ):
411
+ class NodeMethodWithoutValue(View): # pylint: disable=unused-variable
412
+
413
+ def render(self, *args, **kwargs):
414
+ return Document('foo')
415
+
416
+ @View.extension_method('_some_method')
417
+ def bad_method(self, text: str):
418
+ pass
419
+
420
+ with self.assertRaisesRegex(
421
+ TypeError, 'must not have variable positional argument.'
422
+ ):
423
+ class NodeMethodWithVarargs(View): # pylint: disable=unused-variable
424
+
425
+ def render(self, *args, **kwargs):
426
+ return Document('foo')
427
+
428
+ @View.extension_method('_some_method')
429
+ def bad_method(self, value, *args):
430
+ pass
431
+
432
+ class AbstractView(View):
433
+ VIEW_ID = 'abstract'
434
+
435
+ with self.assertRaisesRegex(
436
+ ValueError, 'The `VIEW_ID` .* is the same as the base class'
437
+ ):
438
+ class AbstractViewWithNodeMethod(AbstractView): # pylint: disable=unused-variable
439
+
440
+ def render(self, *args, **kwargs):
441
+ return Document('foo')
442
+
443
+ def test_create(self):
444
+ view = View.create('test')
445
+ self.assertIsInstance(view, TestView)
446
+ with self.assertRaisesRegex(ValueError, 'No view class found'):
447
+ View.create('nonexisted_view')
448
+
449
+ def test_dir(self):
450
+ self.assertIn('test', View.dir())
451
+
452
+ def test_supported_view_classes(self):
453
+ self.assertEqual(
454
+ TestView.Extension.supported_view_classes(),
455
+ {TestView}
456
+ )
457
+ self.assertEqual(
458
+ MyObject.supported_view_classes(),
459
+ {TestView, TestView2}
460
+ )
461
+
462
+ def test_view_options(self):
463
+ view = View.create('test')
464
+ self.assertEqual(view.foo(), 1)
465
+ with base.view_options(text='hello', use_content=True):
466
+ self.assertEqual(
467
+ base.view(MyObject(1, 'a'), view_id='test').content,
468
+ '[hello] 1 a'
469
+ )
470
+
471
+ def test_view_default_behavior(self):
472
+ view = View.create('test')
473
+ self.assertEqual(
474
+ view.render(1).content,
475
+ 'hi 1'
476
+ )
477
+ self.assertEqual(
478
+ view.summary(value=1).content,
479
+ '[Default Summary] abc: 1'
480
+ )
481
+ self.assertEqual(
482
+ view.content(1).content,
483
+ '[Default Content] 1'
484
+ )
485
+ self.assertEqual(
486
+ view.render_key(1).content,
487
+ '[Default Key] 1: None'
488
+ )
489
+ with self.assertRaisesRegex(
490
+ ValueError, 'No value is provided for the `value` argument'
491
+ ):
492
+ view.render()
493
+
494
+ def test_custom_behavior(self):
495
+ view = View.create('test')
496
+ self.assertEqual(
497
+ view.render(MyObject(1, 'foo'), use_content=True).content,
498
+ '[MyObject] 1 foo'
499
+ )
500
+
501
+ self.assertEqual(
502
+ view.render(TestView.Extension(1)).content,
503
+ '[Custom Document] Extension(x=1)'
504
+ )
505
+ self.assertEqual(
506
+ view.summary(TestView.Extension(1)).content,
507
+ '[Custom Summary] Extension(x=1)'
508
+ )
509
+ self.assertEqual(
510
+ view.content(TestView.Extension(1)).content,
511
+ '[Custom Content] Extension(x=1)'
512
+ )
513
+ self.assertEqual(
514
+ view.render_key('foo', value=TestView.Extension(1)).content,
515
+ '[Default Key] foo: Extension(x=1)'
516
+ )
517
+
518
+ view2 = View.create('test2')
519
+ self.assertEqual(
520
+ view2.render(TestView.Extension(1)).content,
521
+ '<h1>Extension(x=1)</h1>'
522
+ )
523
+ self.assertEqual(
524
+ view2.render(MyObject(1, 'foo')).content,
525
+ '<span>1</span>'
526
+ )
527
+ self.assertEqual(
528
+ view2.tooltip(value=MyObject(1, 'foo')).content,
529
+ '<h2>Tooltip</h2>'
530
+ )
531
+
532
+ def test_advanced_routing_with_arg_passing(self):
533
+ view = View.create('test')
534
+ self.assertEqual(
535
+ view.render(
536
+ TestView.Extension(1), use_summary=True).content,
537
+ '[Custom Summary] Extension(x=1)'
538
+ )
539
+ self.assertEqual(
540
+ view.render(
541
+ TestView.Extension(1), use_content=True).content,
542
+ '[Custom Content] Extension(x=1)'
543
+ )
544
+ self.assertEqual(
545
+ view.render(
546
+ TestView.Extension(1),
547
+ use_summary=True,
548
+ default_behavior=True
549
+ ).content,
550
+ '[Default Summary] abc: Extension(x=1)'
551
+ )
552
+ self.assertEqual(
553
+ base.view(
554
+ TestView.Extension(1),
555
+ view_id='test',
556
+ use_summary=True,
557
+ title='def',
558
+ default_behavior=True,
559
+ content_only=True,
560
+ ).content,
561
+ '[Default Summary] def: Extension(x=1)'
562
+ )
563
+
564
+ def test_view_selection_with_multi_inheritance(self):
565
+ self.assertEqual(
566
+ base.view(MyObject(1, 'foo'), view_id='test').content,
567
+ '[Custom Document] MyObject(x=1)'
568
+ )
569
+ self.assertEqual(
570
+ base.view(MyObject(1, 'foo'), view_id='test2').content,
571
+ '<span>1</span>'
572
+ )
573
+
574
+ def test_view_with_content_input(self):
575
+ doc = Document('abc')
576
+ self.assertIs(base.view(doc), doc)
577
+
578
+
579
+ if __name__ == '__main__':
580
+ unittest.main()
@@ -0,0 +1,27 @@
1
+ # Copyright 2024 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
+ """HTML views for PyGlove objects."""
15
+
16
+ # pylint: disable=g-importing-member
17
+ # pylint: disable=g-bad-import-order
18
+
19
+ from pyglove.core.views.html.base import Html
20
+ from pyglove.core.views.html.base import HtmlConvertible
21
+ from pyglove.core.views.html.base import HtmlView
22
+ from pyglove.core.views.html.base import to_html
23
+ from pyglove.core.views.html.base import to_html_str
24
+ from pyglove.core.views.html.tree_view import HtmlTreeView
25
+
26
+ # pylint: enable=g-bad-import-order
27
+ # pylint: enable=g-importing-member