pyglove 0.4.5.dev20240318__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.dev20240318.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.dev20240318.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.dev20240318.dist-info/RECORD +0 -185
- /pyglove/core/{object_utils → utils}/thread_local.py +0 -0
- {pyglove-0.4.5.dev20240318.dist-info → pyglove-0.4.5.dev202501132210.dist-info}/LICENSE +0 -0
- {pyglove-0.4.5.dev20240318.dist-info → pyglove-0.4.5.dev202501132210.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,333 @@
|
|
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 dataclasses
|
15
|
+
import inspect
|
16
|
+
import time
|
17
|
+
import unittest
|
18
|
+
from pyglove.core.coding import errors
|
19
|
+
from pyglove.core.coding import execution
|
20
|
+
from pyglove.core.coding import permissions
|
21
|
+
|
22
|
+
|
23
|
+
class EvaluateTest(unittest.TestCase):
|
24
|
+
|
25
|
+
def test_with_context(self):
|
26
|
+
with execution.context(x=1, y=0):
|
27
|
+
with execution.context(x=2, z=2):
|
28
|
+
self.assertEqual(
|
29
|
+
execution.evaluate(
|
30
|
+
inspect.cleandoc(
|
31
|
+
"""
|
32
|
+
p = x + y + z
|
33
|
+
"""
|
34
|
+
),
|
35
|
+
# Override value from the context.
|
36
|
+
global_vars=dict(z=3),
|
37
|
+
outputs_intermediate=True,
|
38
|
+
),
|
39
|
+
dict(p=2 + 0 + 3, __result__=2 + 0 + 3, __stdout__=''),
|
40
|
+
)
|
41
|
+
|
42
|
+
def test_basics(self):
|
43
|
+
self.assertEqual(
|
44
|
+
execution.evaluate(
|
45
|
+
inspect.cleandoc(
|
46
|
+
"""
|
47
|
+
x = 1
|
48
|
+
y = x + 1
|
49
|
+
print(y)
|
50
|
+
z = x + y
|
51
|
+
"""
|
52
|
+
),
|
53
|
+
outputs_intermediate=True,
|
54
|
+
),
|
55
|
+
dict(x=1, y=2, z=3, __result__=3, __stdout__='2\n'),
|
56
|
+
)
|
57
|
+
self.assertEqual(
|
58
|
+
execution.evaluate(
|
59
|
+
inspect.cleandoc(
|
60
|
+
"""
|
61
|
+
x = 1
|
62
|
+
y = x + 1
|
63
|
+
print(y)
|
64
|
+
z = x + y
|
65
|
+
"""
|
66
|
+
),
|
67
|
+
),
|
68
|
+
3,
|
69
|
+
)
|
70
|
+
with self.assertRaisesRegex(errors.CodeError, 'ValueError'):
|
71
|
+
execution.evaluate(
|
72
|
+
inspect.cleandoc(
|
73
|
+
"""
|
74
|
+
def foo():
|
75
|
+
raise ValueError("intentional error")
|
76
|
+
foo()
|
77
|
+
"""
|
78
|
+
),
|
79
|
+
permission=permissions.CodePermission.ALL
|
80
|
+
)
|
81
|
+
|
82
|
+
def test_class_def(self):
|
83
|
+
ret = execution.evaluate(
|
84
|
+
inspect.cleandoc(
|
85
|
+
"""
|
86
|
+
@dataclasses.dataclass
|
87
|
+
class A:
|
88
|
+
x: int
|
89
|
+
y: int
|
90
|
+
def __call__(self):
|
91
|
+
return self.x + self.y
|
92
|
+
"""
|
93
|
+
),
|
94
|
+
permission=permissions.CodePermission.ALL,
|
95
|
+
global_vars=dict(dataclasses=dataclasses),
|
96
|
+
outputs_intermediate=True,
|
97
|
+
)
|
98
|
+
self.assertEqual(list(ret.keys()), ['A', '__result__', '__stdout__'])
|
99
|
+
self.assertTrue(inspect.isclass(ret['A']))
|
100
|
+
self.assertIs(ret['__result__'], ret['A'])
|
101
|
+
self.assertEqual(ret['__stdout__'], '')
|
102
|
+
|
103
|
+
def test_function_def(self):
|
104
|
+
ret = execution.evaluate(
|
105
|
+
inspect.cleandoc(
|
106
|
+
"""
|
107
|
+
def foo(x, y):
|
108
|
+
return x + y
|
109
|
+
|
110
|
+
def bar(z):
|
111
|
+
return z + foo(z, z)
|
112
|
+
"""
|
113
|
+
),
|
114
|
+
permission=permissions.CodePermission.ALL,
|
115
|
+
outputs_intermediate=True,
|
116
|
+
)
|
117
|
+
self.assertEqual(
|
118
|
+
list(ret.keys()), ['foo', 'bar', '__result__', '__stdout__']
|
119
|
+
)
|
120
|
+
self.assertTrue(inspect.isfunction(ret['foo']))
|
121
|
+
self.assertTrue(inspect.isfunction(ret['bar']))
|
122
|
+
self.assertIs(ret['__result__'], ret['bar'])
|
123
|
+
|
124
|
+
def test_function_def_and_call(self):
|
125
|
+
code = inspect.cleandoc(
|
126
|
+
"""
|
127
|
+
def foo(x, y):
|
128
|
+
return x + y
|
129
|
+
|
130
|
+
def bar(z):
|
131
|
+
print(f'z is {z}')
|
132
|
+
return z + foo(z, z)
|
133
|
+
|
134
|
+
bar(1)
|
135
|
+
"""
|
136
|
+
)
|
137
|
+
ret = execution.evaluate(
|
138
|
+
code,
|
139
|
+
permission=permissions.CodePermission.ALL,
|
140
|
+
outputs_intermediate=True,
|
141
|
+
)
|
142
|
+
self.assertEqual(
|
143
|
+
list(ret.keys()), ['foo', 'bar', '__result__', '__stdout__']
|
144
|
+
)
|
145
|
+
self.assertEqual(ret['__result__'], 3)
|
146
|
+
ret = execution.evaluate(
|
147
|
+
code,
|
148
|
+
permission=permissions.CodePermission.ALL,
|
149
|
+
returns_stdout=True,
|
150
|
+
)
|
151
|
+
self.assertEqual(ret, 'z is 1\n')
|
152
|
+
|
153
|
+
def test_complex(self):
|
154
|
+
ret = execution.evaluate(
|
155
|
+
inspect.cleandoc(
|
156
|
+
"""
|
157
|
+
@dataclasses.dataclass
|
158
|
+
class A:
|
159
|
+
x: int
|
160
|
+
y: int
|
161
|
+
def __call__(self, z):
|
162
|
+
return self.x + self.y + z
|
163
|
+
|
164
|
+
def foo(x, y):
|
165
|
+
return x + y
|
166
|
+
k = A(1, 2)
|
167
|
+
k(foo(3, 4))
|
168
|
+
"""
|
169
|
+
),
|
170
|
+
permission=permissions.CodePermission.ALL,
|
171
|
+
global_vars=dict(dataclasses=dataclasses),
|
172
|
+
outputs_intermediate=True,
|
173
|
+
)
|
174
|
+
self.assertEqual(
|
175
|
+
list(ret.keys()), ['A', 'foo', 'k', '__result__', '__stdout__']
|
176
|
+
)
|
177
|
+
self.assertTrue(inspect.isclass(ret['A']))
|
178
|
+
self.assertTrue(inspect.isfunction(ret['foo']))
|
179
|
+
self.assertIsInstance(ret['k'], ret['A'])
|
180
|
+
self.assertEqual(ret['__result__'], 10)
|
181
|
+
|
182
|
+
def test_run_with_error(self):
|
183
|
+
with self.assertRaisesRegex(
|
184
|
+
errors.CodeError, 'NameError: name .* is not defined'
|
185
|
+
):
|
186
|
+
execution.evaluate(
|
187
|
+
inspect.cleandoc(
|
188
|
+
"""
|
189
|
+
x = 1
|
190
|
+
y = x + z
|
191
|
+
"""
|
192
|
+
),
|
193
|
+
permission=permissions.CodePermission.ALL,
|
194
|
+
)
|
195
|
+
with self.assertRaisesRegex(errors.CodeError, 'ValueError'):
|
196
|
+
execution.evaluate(
|
197
|
+
'raise ValueError()', permission=permissions.CodePermission.ALL
|
198
|
+
)
|
199
|
+
|
200
|
+
|
201
|
+
@dataclasses.dataclass
|
202
|
+
class Foo:
|
203
|
+
x: int
|
204
|
+
y: int
|
205
|
+
|
206
|
+
|
207
|
+
class SandboxCallTest(unittest.TestCase):
|
208
|
+
|
209
|
+
def test_basics(self):
|
210
|
+
def f(x, y):
|
211
|
+
return x + y
|
212
|
+
self.assertEqual(execution.sandbox_call(f, 1, y=2), 3)
|
213
|
+
|
214
|
+
def test_complex_type(self):
|
215
|
+
def f(x, y):
|
216
|
+
return Foo(x, y)
|
217
|
+
|
218
|
+
self.assertEqual(execution.sandbox_call(f, 1, 2), Foo(1, 2))
|
219
|
+
|
220
|
+
def test_timeout(self):
|
221
|
+
def f(x):
|
222
|
+
time.sleep(x)
|
223
|
+
|
224
|
+
self.assertIsNone(execution.sandbox_call(f, 0, timeout=1))
|
225
|
+
with self.assertRaises(TimeoutError):
|
226
|
+
execution.sandbox_call(f, 2, timeout=1)
|
227
|
+
|
228
|
+
def test_raise(self):
|
229
|
+
def f(x):
|
230
|
+
if x == 0:
|
231
|
+
raise ValueError()
|
232
|
+
|
233
|
+
self.assertIsNone(execution.sandbox_call(f, 1))
|
234
|
+
with self.assertRaises(ValueError):
|
235
|
+
execution.sandbox_call(f, 0)
|
236
|
+
|
237
|
+
|
238
|
+
class CallTest(unittest.TestCase):
|
239
|
+
|
240
|
+
def test_call_without_sandboxing(self):
|
241
|
+
def foo(x, y):
|
242
|
+
return x + y
|
243
|
+
|
244
|
+
self.assertEqual(
|
245
|
+
execution.maybe_sandbox_call(foo, 1, y=2, sandbox=False),
|
246
|
+
3
|
247
|
+
)
|
248
|
+
|
249
|
+
def test_call_with_sandboxing(self):
|
250
|
+
def foo(x, y):
|
251
|
+
return x + y
|
252
|
+
|
253
|
+
self.assertEqual(
|
254
|
+
execution.maybe_sandbox_call(foo, 1, y=2, sandbox=True),
|
255
|
+
3
|
256
|
+
)
|
257
|
+
|
258
|
+
def make_cls():
|
259
|
+
@dataclasses.dataclass
|
260
|
+
class A:
|
261
|
+
x: str
|
262
|
+
return A
|
263
|
+
|
264
|
+
with self.assertRaises(errors.SerializationError):
|
265
|
+
execution.maybe_sandbox_call(make_cls, sandbox=True)
|
266
|
+
|
267
|
+
def test_call_with_automatic_sandboxing(self):
|
268
|
+
def foo(x, y):
|
269
|
+
return x + y
|
270
|
+
|
271
|
+
self.assertEqual(
|
272
|
+
execution.maybe_sandbox_call(foo, 1, y=2),
|
273
|
+
3
|
274
|
+
)
|
275
|
+
|
276
|
+
def make_cls():
|
277
|
+
@dataclasses.dataclass
|
278
|
+
class A:
|
279
|
+
x: str
|
280
|
+
return A
|
281
|
+
|
282
|
+
self.assertTrue(inspect.isclass(execution.maybe_sandbox_call(make_cls)))
|
283
|
+
|
284
|
+
|
285
|
+
class RunTest(unittest.TestCase):
|
286
|
+
|
287
|
+
def test_run_without_sandboxing(self):
|
288
|
+
self.assertEqual(
|
289
|
+
execution.run(
|
290
|
+
'x + y',
|
291
|
+
global_vars=dict(x=1, y=2),
|
292
|
+
sandbox=False,
|
293
|
+
),
|
294
|
+
3,
|
295
|
+
)
|
296
|
+
|
297
|
+
def test_run_with_sandboxing(self):
|
298
|
+
self.assertEqual(
|
299
|
+
execution.run(
|
300
|
+
'x + y',
|
301
|
+
global_vars=dict(x=1, y=2),
|
302
|
+
sandbox=True,
|
303
|
+
),
|
304
|
+
3,
|
305
|
+
)
|
306
|
+
|
307
|
+
def test_run_with_automatic_sandboxing(self):
|
308
|
+
self.assertEqual(
|
309
|
+
execution.run(
|
310
|
+
'x + y',
|
311
|
+
global_vars=dict(x=1, y=2),
|
312
|
+
),
|
313
|
+
3,
|
314
|
+
)
|
315
|
+
|
316
|
+
r = execution.run(
|
317
|
+
inspect.cleandoc("""
|
318
|
+
def foo(x, y):
|
319
|
+
return x + y
|
320
|
+
|
321
|
+
@dataclasses.dataclass
|
322
|
+
class A:
|
323
|
+
x: str
|
324
|
+
"""),
|
325
|
+
global_vars=dict(dataclasses=dataclasses),
|
326
|
+
outputs_intermediate=True,
|
327
|
+
)
|
328
|
+
self.assertTrue(inspect.isfunction(r['foo']))
|
329
|
+
self.assertTrue(inspect.isclass(r['A']))
|
330
|
+
|
331
|
+
|
332
|
+
if __name__ == '__main__':
|
333
|
+
unittest.main()
|
@@ -11,10 +11,16 @@
|
|
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
|
-
"""Utilities for
|
14
|
+
"""Utilities for function generation."""
|
15
15
|
|
16
16
|
from typing import Any, Dict, List, Optional
|
17
|
-
|
17
|
+
|
18
|
+
|
19
|
+
class _NoTypeAnnotation:
|
20
|
+
"""Placeholder for no type annotation."""
|
21
|
+
|
22
|
+
|
23
|
+
NO_TYPE_ANNOTATION = _NoTypeAnnotation()
|
18
24
|
|
19
25
|
|
20
26
|
def make_function(
|
@@ -24,11 +30,11 @@ def make_function(
|
|
24
30
|
*,
|
25
31
|
exec_globals: Optional[Dict[str, Any]] = None,
|
26
32
|
exec_locals: Optional[Dict[str, Any]] = None,
|
27
|
-
return_type: Any =
|
33
|
+
return_type: Any = NO_TYPE_ANNOTATION):
|
28
34
|
"""Creates a function dynamically from source."""
|
29
35
|
if exec_locals is None:
|
30
36
|
exec_locals = {}
|
31
|
-
if return_type !=
|
37
|
+
if return_type != NO_TYPE_ANNOTATION:
|
32
38
|
exec_locals['_return_type'] = return_type
|
33
39
|
return_annotation = '->_return_type'
|
34
40
|
else:
|
@@ -1,4 +1,4 @@
|
|
1
|
-
# Copyright
|
1
|
+
# Copyright 2024 The PyGlove Authors
|
2
2
|
#
|
3
3
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
4
|
# you may not use this file except in compliance with the License.
|
@@ -11,20 +11,18 @@
|
|
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.codegen."""
|
15
|
-
|
16
14
|
import inspect
|
17
15
|
import typing
|
18
16
|
import unittest
|
19
17
|
|
20
|
-
from pyglove.core.
|
18
|
+
from pyglove.core.coding import function_generation
|
21
19
|
|
22
20
|
|
23
21
|
class MakeFunctionTest(unittest.TestCase):
|
24
|
-
"""Tests for
|
22
|
+
"""Tests for function_generation.make_function."""
|
25
23
|
|
26
24
|
def test_make_function_with_type_annotations(self):
|
27
|
-
func =
|
25
|
+
func = function_generation.make_function(
|
28
26
|
'foo',
|
29
27
|
['x: typing.Optional[int]', 'y: int = 0'],
|
30
28
|
['return x + y'],
|
@@ -41,7 +39,7 @@ class MakeFunctionTest(unittest.TestCase):
|
|
41
39
|
self.assertEqual(func(1, 2), 3)
|
42
40
|
|
43
41
|
def test_make_function_without_type_annotations(self):
|
44
|
-
func =
|
42
|
+
func = function_generation.make_function(
|
45
43
|
'foo',
|
46
44
|
['x', 'y'],
|
47
45
|
['return x + y'])
|
@@ -0,0 +1,153 @@
|
|
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
|
+
"""Python code parser with permission control."""
|
15
|
+
|
16
|
+
import ast
|
17
|
+
import sys
|
18
|
+
from typing import Optional
|
19
|
+
|
20
|
+
from pyglove.core.coding import errors
|
21
|
+
from pyglove.core.coding import permissions
|
22
|
+
|
23
|
+
|
24
|
+
class _CodeValidator(ast.NodeVisitor):
|
25
|
+
"""Python AST node visitor for ensuring code are permitted."""
|
26
|
+
|
27
|
+
def __init__(
|
28
|
+
self,
|
29
|
+
code: str,
|
30
|
+
permission: permissions.CodePermission
|
31
|
+
):
|
32
|
+
super().__init__()
|
33
|
+
self.code = code
|
34
|
+
self.permission = permission
|
35
|
+
|
36
|
+
def verify(
|
37
|
+
self,
|
38
|
+
node,
|
39
|
+
flag: permissions.CodePermission,
|
40
|
+
node_type,
|
41
|
+
error_message: str,
|
42
|
+
) -> None:
|
43
|
+
"""Verifies if the node type is permitted based on flag."""
|
44
|
+
if isinstance(node_type, (tuple, list)):
|
45
|
+
node_type = tuple(t for t in node_type if t is not None)
|
46
|
+
if isinstance(node, node_type) and not (self.permission & flag):
|
47
|
+
if sys.version_info >= (3, 10):
|
48
|
+
error = SyntaxError(
|
49
|
+
error_message,
|
50
|
+
(
|
51
|
+
'<generated-code>',
|
52
|
+
node.lineno,
|
53
|
+
node.col_offset,
|
54
|
+
self._code_line(node.lineno),
|
55
|
+
node.end_lineno,
|
56
|
+
node.end_col_offset,
|
57
|
+
)
|
58
|
+
)
|
59
|
+
else:
|
60
|
+
error = SyntaxError(
|
61
|
+
error_message,
|
62
|
+
(
|
63
|
+
'<generated-code>',
|
64
|
+
node.lineno,
|
65
|
+
node.col_offset,
|
66
|
+
self._code_line(node.lineno),
|
67
|
+
)
|
68
|
+
)
|
69
|
+
setattr(error, 'end_lineno', node.lineno)
|
70
|
+
setattr(error, 'end_col_offset', node.col_offset)
|
71
|
+
raise error
|
72
|
+
|
73
|
+
def _code_line(self, lineno):
|
74
|
+
return self.code.split('\n')[lineno - 1]
|
75
|
+
|
76
|
+
def generic_visit(self, node):
|
77
|
+
self.verify(
|
78
|
+
node,
|
79
|
+
permissions.CodePermission.ASSIGN,
|
80
|
+
(ast.Assign),
|
81
|
+
'Assignment is not allowed.',
|
82
|
+
)
|
83
|
+
|
84
|
+
self.verify(
|
85
|
+
node,
|
86
|
+
permissions.CodePermission.CONDITION,
|
87
|
+
# Match is not supported until Python 3.10.
|
88
|
+
(ast.If, getattr(ast, 'Match', None)),
|
89
|
+
'Condition is not allowed.',
|
90
|
+
)
|
91
|
+
|
92
|
+
self.verify(
|
93
|
+
node,
|
94
|
+
permissions.CodePermission.LOOP,
|
95
|
+
(ast.For, ast.While, ast.AsyncFor, ast.AsyncWith),
|
96
|
+
'Loop is not allowed.',
|
97
|
+
)
|
98
|
+
|
99
|
+
self.verify(
|
100
|
+
node,
|
101
|
+
permissions.CodePermission.EXCEPTION,
|
102
|
+
(ast.Try, ast.Raise, ast.Assert),
|
103
|
+
'Exception is not allowed.',
|
104
|
+
)
|
105
|
+
|
106
|
+
self.verify(
|
107
|
+
node,
|
108
|
+
permissions.CodePermission.CALL,
|
109
|
+
ast.Call,
|
110
|
+
'Call is not allowed.',
|
111
|
+
)
|
112
|
+
self.verify(
|
113
|
+
node,
|
114
|
+
permissions.CodePermission.CLASS_DEFINITION,
|
115
|
+
ast.ClassDef,
|
116
|
+
'Class definition is not allowed.',
|
117
|
+
)
|
118
|
+
|
119
|
+
self.verify(
|
120
|
+
node,
|
121
|
+
permissions.CodePermission.FUNCTION_DEFINITION,
|
122
|
+
(
|
123
|
+
ast.FunctionDef,
|
124
|
+
ast.AsyncFunctionDef,
|
125
|
+
ast.Lambda,
|
126
|
+
ast.Return,
|
127
|
+
ast.Yield,
|
128
|
+
ast.YieldFrom,
|
129
|
+
),
|
130
|
+
'Function definition is not allowed.',
|
131
|
+
)
|
132
|
+
|
133
|
+
self.verify(
|
134
|
+
node,
|
135
|
+
permissions.CodePermission.IMPORT,
|
136
|
+
(ast.Import, ast.ImportFrom),
|
137
|
+
'`import` is not allowed.',
|
138
|
+
)
|
139
|
+
|
140
|
+
super().generic_visit(node)
|
141
|
+
|
142
|
+
|
143
|
+
def parse(
|
144
|
+
code: str,
|
145
|
+
permission: Optional[permissions.CodePermission] = None,
|
146
|
+
) -> ast.AST:
|
147
|
+
try:
|
148
|
+
parsed_code = ast.parse(code, mode='exec')
|
149
|
+
if permission is not None:
|
150
|
+
_CodeValidator(code, permission).visit(parsed_code)
|
151
|
+
except SyntaxError as e:
|
152
|
+
raise errors.CodeError(code, e) from e
|
153
|
+
return parsed_code
|
@@ -0,0 +1,150 @@
|
|
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 unittest
|
16
|
+
from pyglove.core.coding import errors
|
17
|
+
from pyglove.core.coding import parsing
|
18
|
+
from pyglove.core.coding import permissions
|
19
|
+
|
20
|
+
|
21
|
+
class ParsePythonCodeTest(unittest.TestCase):
|
22
|
+
|
23
|
+
def test_no_permission_check(self):
|
24
|
+
ast = parsing.parse(inspect.cleandoc("""
|
25
|
+
if x > 0:
|
26
|
+
print(x)
|
27
|
+
"""))
|
28
|
+
self.assertIsNotNone(ast)
|
29
|
+
|
30
|
+
def assert_allowed(self, code: str, permission: permissions.CodePermission):
|
31
|
+
ast = parsing.parse(inspect.cleandoc(code), permission)
|
32
|
+
self.assertIsNotNone(ast)
|
33
|
+
|
34
|
+
def assert_not_allowed(
|
35
|
+
self, code: str, permission: permissions.CodePermission
|
36
|
+
):
|
37
|
+
with self.assertRaisesRegex(errors.CodeError, '.* is not allowed'):
|
38
|
+
parsing.parse(inspect.cleandoc(code), permission)
|
39
|
+
|
40
|
+
def test_parse_with_allowed_code(self):
|
41
|
+
self.assert_allowed(
|
42
|
+
"""
|
43
|
+
x = 1
|
44
|
+
""",
|
45
|
+
permissions.CodePermission.ASSIGN,
|
46
|
+
)
|
47
|
+
self.assert_allowed(
|
48
|
+
"""
|
49
|
+
if x > 0:
|
50
|
+
pass
|
51
|
+
""",
|
52
|
+
permissions.CodePermission.CONDITION,
|
53
|
+
)
|
54
|
+
self.assert_allowed(
|
55
|
+
"""
|
56
|
+
for i in [1, 2, 3]:
|
57
|
+
pass
|
58
|
+
""",
|
59
|
+
permissions.CodePermission.LOOP,
|
60
|
+
)
|
61
|
+
self.assert_allowed(
|
62
|
+
"""
|
63
|
+
foo(x=1, y=bar(2))
|
64
|
+
""",
|
65
|
+
permissions.CodePermission.CALL,
|
66
|
+
)
|
67
|
+
self.assert_allowed(
|
68
|
+
"""
|
69
|
+
assert x > 1
|
70
|
+
""",
|
71
|
+
permissions.CodePermission.EXCEPTION,
|
72
|
+
)
|
73
|
+
self.assert_allowed(
|
74
|
+
"""
|
75
|
+
class A:
|
76
|
+
pass
|
77
|
+
""",
|
78
|
+
permissions.CodePermission.CLASS_DEFINITION,
|
79
|
+
)
|
80
|
+
self.assert_allowed(
|
81
|
+
"""
|
82
|
+
def foo(x, y):
|
83
|
+
return x + y
|
84
|
+
""",
|
85
|
+
permissions.CodePermission.FUNCTION_DEFINITION,
|
86
|
+
)
|
87
|
+
self.assert_allowed(
|
88
|
+
"""
|
89
|
+
import re
|
90
|
+
""",
|
91
|
+
permissions.CodePermission.IMPORT,
|
92
|
+
)
|
93
|
+
|
94
|
+
def test_parse_with_not_allowed_code(self):
|
95
|
+
self.assert_not_allowed(
|
96
|
+
"""
|
97
|
+
x = 1
|
98
|
+
""",
|
99
|
+
permissions.CodePermission.CONDITION,
|
100
|
+
)
|
101
|
+
self.assert_not_allowed(
|
102
|
+
"""
|
103
|
+
if x > 0:
|
104
|
+
pass
|
105
|
+
""",
|
106
|
+
permissions.CodePermission.BASIC,
|
107
|
+
)
|
108
|
+
self.assert_not_allowed(
|
109
|
+
"""
|
110
|
+
for i in range(5):
|
111
|
+
pass
|
112
|
+
""",
|
113
|
+
permissions.CodePermission.BASIC,
|
114
|
+
)
|
115
|
+
self.assert_not_allowed(
|
116
|
+
"""
|
117
|
+
assert x > 1
|
118
|
+
""",
|
119
|
+
permissions.CodePermission.BASIC,
|
120
|
+
)
|
121
|
+
self.assert_not_allowed(
|
122
|
+
"""
|
123
|
+
class A:
|
124
|
+
pass
|
125
|
+
""",
|
126
|
+
permissions.CodePermission.BASIC,
|
127
|
+
)
|
128
|
+
self.assert_not_allowed(
|
129
|
+
"""
|
130
|
+
def foo(x, y):
|
131
|
+
return x + y
|
132
|
+
""",
|
133
|
+
permissions.CodePermission.BASIC,
|
134
|
+
)
|
135
|
+
self.assert_not_allowed(
|
136
|
+
"""
|
137
|
+
import re
|
138
|
+
""",
|
139
|
+
permissions.CodePermission.BASIC,
|
140
|
+
)
|
141
|
+
self.assert_not_allowed(
|
142
|
+
"""
|
143
|
+
range(5)
|
144
|
+
""",
|
145
|
+
permissions.CodePermission.ASSIGN,
|
146
|
+
)
|
147
|
+
|
148
|
+
|
149
|
+
if __name__ == '__main__':
|
150
|
+
unittest.main()
|