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.
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.dev20240318.dist-info → pyglove-0.4.5.dev202501132210.dist-info}/METADATA +18 -4
  131. pyglove-0.4.5.dev202501132210.dist-info/RECORD +214 -0
  132. {pyglove-0.4.5.dev20240318.dist-info → pyglove-0.4.5.dev202501132210.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.dev20240318.dist-info/RECORD +0 -185
  143. /pyglove/core/{object_utils → utils}/thread_local.py +0 -0
  144. {pyglove-0.4.5.dev20240318.dist-info → pyglove-0.4.5.dev202501132210.dist-info}/LICENSE +0 -0
  145. {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 code generation."""
14
+ """Utilities for function generation."""
15
15
 
16
16
  from typing import Any, Dict, List, Optional
17
- from pyglove.core.object_utils.missing import MISSING_VALUE
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 = MISSING_VALUE):
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 != MISSING_VALUE:
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 2022 The PyGlove Authors
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.object_utils import codegen
18
+ from pyglove.core.coding import function_generation
21
19
 
22
20
 
23
21
  class MakeFunctionTest(unittest.TestCase):
24
- """Tests for codegen.make_function."""
22
+ """Tests for function_generation.make_function."""
25
23
 
26
24
  def test_make_function_with_type_annotations(self):
27
- func = codegen.make_function(
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 = codegen.make_function(
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()