pyglove 0.4.5.dev20240319__py3-none-any.whl → 0.4.5.dev202501132210__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 +54 -20
- 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 +309 -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 +54 -41
- pyglove/core/geno/base_test.py +2 -4
- pyglove/core/geno/categorical.py +37 -28
- pyglove/core/geno/custom.py +19 -16
- pyglove/core/geno/numerical.py +20 -17
- pyglove/core/geno/space.py +4 -5
- pyglove/core/hyper/base.py +6 -6
- pyglove/core/hyper/categorical.py +94 -55
- 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 +2 -4
- pyglove/core/hyper/dynamic_evaluation.py +5 -6
- 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/io/__init__.py +1 -0
- pyglove/core/io/file_system.py +17 -7
- pyglove/core/io/file_system_test.py +2 -0
- pyglove/core/io/sequence.py +299 -0
- pyglove/core/io/sequence_test.py +124 -0
- 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 +17 -5
- pyglove/core/patching/rule_based_test.py +27 -4
- pyglove/core/symbolic/__init__.py +2 -7
- pyglove/core/symbolic/base.py +320 -183
- pyglove/core/symbolic/base_test.py +123 -19
- pyglove/core/symbolic/boilerplate.py +7 -13
- pyglove/core/symbolic/boilerplate_test.py +25 -23
- pyglove/core/symbolic/class_wrapper.py +48 -45
- pyglove/core/symbolic/class_wrapper_test.py +2 -2
- pyglove/core/symbolic/compounding.py +9 -15
- pyglove/core/symbolic/compounding_test.py +2 -4
- pyglove/core/symbolic/dict.py +154 -110
- pyglove/core/symbolic/dict_test.py +238 -130
- pyglove/core/symbolic/diff.py +199 -10
- pyglove/core/symbolic/diff_test.py +226 -0
- pyglove/core/symbolic/flags.py +1 -1
- pyglove/core/symbolic/functor.py +29 -26
- pyglove/core/symbolic/functor_test.py +102 -50
- pyglove/core/symbolic/inferred.py +2 -2
- pyglove/core/symbolic/list.py +81 -50
- pyglove/core/symbolic/list_test.py +119 -97
- pyglove/core/symbolic/object.py +225 -113
- pyglove/core/symbolic/object_test.py +320 -108
- pyglove/core/symbolic/origin.py +17 -14
- pyglove/core/symbolic/origin_test.py +4 -2
- pyglove/core/symbolic/pure_symbolic.py +4 -3
- pyglove/core/symbolic/ref.py +108 -21
- pyglove/core/symbolic/ref_test.py +93 -0
- pyglove/core/symbolic/symbolize_test.py +10 -2
- pyglove/core/tuning/local_backend.py +2 -2
- pyglove/core/tuning/protocols.py +3 -3
- pyglove/core/tuning/sample_test.py +3 -3
- pyglove/core/typing/__init__.py +14 -5
- pyglove/core/typing/annotation_conversion.py +43 -27
- pyglove/core/typing/annotation_conversion_test.py +23 -0
- pyglove/core/typing/callable_ext.py +241 -3
- pyglove/core/typing/callable_ext_test.py +255 -0
- pyglove/core/typing/callable_signature.py +510 -66
- pyglove/core/typing/callable_signature_test.py +619 -99
- pyglove/core/typing/class_schema.py +229 -154
- pyglove/core/typing/class_schema_test.py +149 -95
- pyglove/core/typing/custom_typing.py +5 -4
- pyglove/core/typing/inspect.py +63 -0
- pyglove/core/typing/inspect_test.py +39 -0
- pyglove/core/typing/key_specs.py +10 -11
- pyglove/core/typing/key_specs_test.py +7 -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 +604 -362
- pyglove/core/typing/value_specs_test.py +328 -90
- pyglove/core/utils/__init__.py +164 -0
- pyglove/core/{object_utils → utils}/common_traits.py +3 -67
- pyglove/core/utils/common_traits_test.py +36 -0
- pyglove/core/{object_utils → utils}/docstr_utils.py +23 -0
- pyglove/core/{object_utils → utils}/docstr_utils_test.py +36 -4
- pyglove/core/{object_utils → utils}/error_utils.py +78 -9
- pyglove/core/{object_utils → utils}/error_utils_test.py +61 -5
- pyglove/core/utils/formatting.py +464 -0
- pyglove/core/utils/formatting_test.py +453 -0
- 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 +177 -52
- pyglove/core/{object_utils → utils}/json_conversion_test.py +97 -16
- pyglove/core/{object_utils → utils}/missing.py +3 -3
- 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/utils/timing.py +236 -0
- pyglove/core/utils/timing_test.py +154 -0
- pyglove/core/{object_utils → utils}/value_location.py +275 -6
- pyglove/core/utils/value_location_test.py +707 -0
- pyglove/core/views/__init__.py +32 -0
- pyglove/core/views/base.py +804 -0
- pyglove/core/views/base_test.py +580 -0
- pyglove/core/views/html/__init__.py +27 -0
- pyglove/core/views/html/base.py +547 -0
- pyglove/core/views/html/base_test.py +830 -0
- pyglove/core/views/html/controls/__init__.py +35 -0
- pyglove/core/views/html/controls/base.py +275 -0
- pyglove/core/views/html/controls/label.py +207 -0
- pyglove/core/views/html/controls/label_test.py +157 -0
- pyglove/core/views/html/controls/progress_bar.py +183 -0
- pyglove/core/views/html/controls/progress_bar_test.py +97 -0
- pyglove/core/views/html/controls/tab.py +320 -0
- pyglove/core/views/html/controls/tab_test.py +87 -0
- pyglove/core/views/html/controls/tooltip.py +99 -0
- pyglove/core/views/html/controls/tooltip_test.py +99 -0
- pyglove/core/views/html/tree_view.py +1517 -0
- pyglove/core/views/html/tree_view_test.py +1461 -0
- {pyglove-0.4.5.dev20240319.dist-info → pyglove-0.4.5.dev202501132210.dist-info}/METADATA +18 -4
- pyglove-0.4.5.dev202501132210.dist-info/RECORD +214 -0
- {pyglove-0.4.5.dev20240319.dist-info → pyglove-0.4.5.dev202501132210.dist-info}/WHEEL +1 -1
- pyglove/core/object_utils/__init__.py +0 -154
- pyglove/core/object_utils/common_traits_test.py +0 -82
- pyglove/core/object_utils/formatting.py +0 -234
- pyglove/core/object_utils/formatting_test.py +0 -223
- pyglove/core/object_utils/value_location_test.py +0 -385
- pyglove/core/symbolic/schema_utils.py +0 -327
- pyglove/core/symbolic/schema_utils_test.py +0 -57
- pyglove/core/typing/class_schema_utils.py +0 -202
- pyglove/core/typing/class_schema_utils_test.py +0 -194
- pyglove-0.4.5.dev20240319.dist-info/RECORD +0 -185
- /pyglove/core/{object_utils → utils}/thread_local.py +0 -0
- {pyglove-0.4.5.dev20240319.dist-info → pyglove-0.4.5.dev202501132210.dist-info}/LICENSE +0 -0
- {pyglove-0.4.5.dev20240319.dist-info → pyglove-0.4.5.dev202501132210.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
|