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
pyglove/core/__init__.py CHANGED
@@ -37,8 +37,7 @@ Here lists the sub-modules included in the core PyGlove library:
37
37
  |__ tuning : Interface for program tuning with a local backend.
38
38
  |__ detouring : Detouring classes creation without symbolic types.
39
39
  |__ patching : Patching a program with URL-like strings.
40
- |__ object_utils : Utility libary on operating with Python objects.
41
-
40
+ |__ utils : Utility libary on operating with Python objects.
42
41
  """
43
42
 
44
43
  # NOTE(daiyip): We disable bad-import-order to preserve the relation of
@@ -163,6 +162,7 @@ to_json = symbolic.to_json
163
162
  to_json_str = symbolic.to_json_str
164
163
  save = symbolic.save
165
164
  load = symbolic.load
165
+ open_jsonl = symbolic.open_jsonl
166
166
  get_load_handler = symbolic.get_load_handler
167
167
  set_load_handler = symbolic.set_load_handler
168
168
  get_save_handler = symbolic.get_save_handler
@@ -192,7 +192,8 @@ CustomTyping = typing.CustomTyping
192
192
 
193
193
  get_converter = typing.get_converter
194
194
  register_converter = typing.register_converter
195
- get_signature = typing.get_signature
195
+ signature = typing.signature
196
+ schema = typing.schema
196
197
 
197
198
 
198
199
  #
@@ -271,29 +272,56 @@ ObjectFactory = patching.ObjectFactory
271
272
 
272
273
 
273
274
  #
274
- # Symbols from 'object_utils' sub-module.
275
+ # Symbols from 'utils' sub-module.
275
276
  #
276
277
 
277
- from pyglove.core import object_utils
278
- KeyPath = object_utils.KeyPath
279
- MISSING_VALUE = object_utils.MISSING_VALUE
278
+ from pyglove.core import utils
279
+
280
+ # For backward compatibility.
281
+ object_utils = utils
282
+
283
+ KeyPath = utils.KeyPath
284
+ KeyPathSet = utils.KeyPathSet
285
+ MISSING_VALUE = utils.MISSING_VALUE
286
+
287
+ Formattable = utils.Formattable
288
+ repr_format = utils.repr_format
289
+ str_format = utils.str_format
290
+
291
+ MaybePartial = utils.MaybePartial
292
+ JSONConvertible = utils.JSONConvertible
293
+ DocStr = utils.DocStr
294
+
295
+ registered_types = utils.registered_types
296
+ explicit_method_override = utils.explicit_method_override
280
297
 
281
- Formattable = object_utils.Formattable
282
- repr_format = object_utils.repr_format
283
- str_format = object_utils.str_format
298
+ is_partial = utils.is_partial
299
+ format = utils.format # pylint: disable=redefined-builtin
300
+ print = utils.print # pylint: disable=redefined-builtin
301
+ docstr = utils.docstr
302
+ catch_errors = utils.catch_errors
303
+ timeit = utils.timeit
284
304
 
285
- MaybePartial = object_utils.MaybePartial
286
- JSONConvertible = object_utils.JSONConvertible
287
- DocStr = object_utils.DocStr
305
+ colored = utils.colored
306
+ decolor = utils.decolor
288
307
 
289
- registered_types = object_utils.registered_types
290
- explicit_method_override = object_utils.explicit_method_override
308
+ # Symbols from 'views' sub-module.
309
+
310
+ from pyglove.core import views
311
+ view = views.view
312
+ view_options = views.view_options
313
+ View = views.View
314
+ Html = views.Html
315
+ to_html = views.to_html
316
+ to_html_str = views.to_html_str
317
+
318
+ # NOTE(daiyip): Hack to add `controls` to `pg.views.html`.
319
+ # We exclude `html.controls` from `pyglove.core.views.html` package to avoid
320
+ # circular dependency between `pyglove.core.views.html` and
321
+ # `pyglove.core.symbolic`.
322
+ from pyglove.core.views.html import controls
323
+ views.html.controls = controls
291
324
 
292
- is_partial = object_utils.is_partial
293
- format = object_utils.format # pylint: disable=redefined-builtin
294
- print = object_utils.print # pylint: disable=redefined-builtin
295
- docstr = object_utils.docstr
296
- catch_errors = object_utils.catch_errors
297
325
 
298
326
  #
299
327
  # Symbols from `io` sub-module.
@@ -301,6 +329,12 @@ catch_errors = object_utils.catch_errors
301
329
 
302
330
  from pyglove.core import io
303
331
 
332
+ #
333
+ # Symbols from `coding` sub-module.
334
+ #
335
+ #
336
+
337
+ from pyglove.core import coding
304
338
 
305
339
  #
306
340
  # Symbols from `logging.py`.
@@ -0,0 +1,42 @@
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
+ # pylint: disable=line-too-long
15
+ """Code generation utilities."""
16
+
17
+ # pylint: enable=line-too-long
18
+ # pylint: disable=g-bad-import-order
19
+ # pylint: disable=g-importing-member
20
+
21
+ from pyglove.core.coding.errors import CodeError
22
+ from pyglove.core.coding.errors import SerializationError
23
+
24
+ from pyglove.core.coding.permissions import CodePermission
25
+ from pyglove.core.coding.permissions import permission
26
+ from pyglove.core.coding.permissions import get_permission
27
+
28
+ from pyglove.core.coding.parsing import parse
29
+
30
+ from pyglove.core.coding.execution import context
31
+ from pyglove.core.coding.execution import get_context
32
+ from pyglove.core.coding.execution import evaluate
33
+ from pyglove.core.coding.execution import sandbox_call
34
+ from pyglove.core.coding.execution import maybe_sandbox_call
35
+ from pyglove.core.coding.execution import run
36
+
37
+ from pyglove.core.coding.function_generation import NO_TYPE_ANNOTATION
38
+ from pyglove.core.coding.function_generation import make_function
39
+
40
+ # pylint: disable=line-too-long
41
+ # pylint: enable=g-bad-import-order
42
+ # pylint: enable=g-importing-member
@@ -0,0 +1,111 @@
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 errors."""
15
+
16
+ import io
17
+ import sys
18
+ import textwrap
19
+ import traceback
20
+ from typing import Optional
21
+
22
+ from pyglove.core import utils
23
+
24
+
25
+ class CodeError(RuntimeError):
26
+ """Python code error."""
27
+
28
+ def __init__(
29
+ self,
30
+ code: str,
31
+ cause: Exception,
32
+ ):
33
+ self.code = code
34
+ self.cause = cause
35
+
36
+ # Figure out the starting and ending line numbers of the erratic code.
37
+ lineno = None
38
+ end_lineno = None
39
+ if isinstance(cause, SyntaxError):
40
+ lineno = cause.lineno
41
+ # For Python 3.9 and below, `end_lineno` is not available.
42
+ end_lineno = getattr(cause, 'end_lineno', lineno)
43
+ elif not isinstance(cause, TimeoutError):
44
+ tb = sys.exc_info()[2]
45
+ frames = traceback.extract_tb(tb, limit=5)
46
+ for f in frames:
47
+ if not f.filename or f.filename == '<string>':
48
+ lineno = f.lineno
49
+ end_lineno = lineno
50
+ break
51
+ self.lineno = lineno
52
+ self.end_lineno = end_lineno
53
+
54
+ def __str__(self):
55
+ return self.format(include_complete_code=True)
56
+
57
+ def code_lines(self, start_line: int, end_line: int):
58
+ """Returns code lines ."""
59
+ return '\n'.join(self.code.split('\n')[start_line:end_line])
60
+
61
+ def format(self, include_complete_code: bool = True):
62
+ """Formats the code error."""
63
+ r = io.StringIO()
64
+ error_message = str(self.cause).rstrip()
65
+ if 'line' not in error_message and self.lineno is not None:
66
+ error_message += f' (<unknown>, line {self.lineno})'
67
+ r.write(
68
+ utils.colored(
69
+ f'{self.cause.__class__.__name__}: {error_message}', 'magenta'))
70
+
71
+ if self.lineno is not None:
72
+ r.write('\n\n')
73
+ r.write(textwrap.indent(
74
+ utils.colored(
75
+ self.code_lines(self.lineno - 1, self.end_lineno), 'magenta'),
76
+ ' ' * 2
77
+ ))
78
+ r.write('\n')
79
+
80
+ if include_complete_code:
81
+ r.write('\n')
82
+ r.write(utils.colored('[Code]', 'green', styles=['bold']))
83
+ r.write('\n\n')
84
+ r.write(utils.colored(' ```python\n', 'green'))
85
+ r.write(textwrap.indent(
86
+ utils.colored(self.code, 'green'),
87
+ ' ' * 2
88
+ ))
89
+ r.write(utils.colored('\n ```\n', 'green'))
90
+ return r.getvalue()
91
+
92
+
93
+ class SerializationError(RuntimeError):
94
+ """Object serialization error."""
95
+
96
+ def __init__(self, message: Optional[str], cause: Exception):
97
+ self.message = message
98
+ self.cause = cause
99
+
100
+ def __str__(self):
101
+ r = io.StringIO()
102
+ cause_message = str(self.cause).rstrip()
103
+ if self.message:
104
+ r.write(utils.colored(self.message, 'magenta'))
105
+ r.write('\n\n')
106
+ r.write(
107
+ utils.colored(
108
+ f'{self.cause.__class__.__name__}: {cause_message}', 'magenta'
109
+ )
110
+ )
111
+ return r.getvalue()
@@ -0,0 +1,98 @@
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
+
17
+ from pyglove.core.coding import errors
18
+ from pyglove.core.coding import execution
19
+
20
+
21
+ def code_error(code: str) -> errors.CodeError:
22
+ try:
23
+ execution.run(inspect.cleandoc(code), timeout=2)
24
+ assert False, 'should not reach here'
25
+ except errors.CodeError as e:
26
+ return e
27
+
28
+
29
+ class CodeErrorsTest(unittest.TestCase):
30
+
31
+ def test_format(self):
32
+ e = code_error(
33
+ """
34
+ x = y + 1
35
+ """
36
+ )
37
+ self.assertIn('[Code]', str(e))
38
+ self.assertNotIn(
39
+ '[Code]', e.format(include_complete_code=False))
40
+
41
+ def test_lineno(self):
42
+ self.assertEqual(
43
+ code_error(
44
+ """
45
+ x = y + 1
46
+ """
47
+ ).lineno, 1)
48
+ self.assertEqual(
49
+ code_error(
50
+ """
51
+ x = 1
52
+ for i of x:
53
+ y = i
54
+ """
55
+ ).lineno, 2)
56
+ self.assertEqual(
57
+ code_error(
58
+ """
59
+ x = 1
60
+ y = 2
61
+ raise ValueError
62
+ """
63
+ ).lineno, 3)
64
+
65
+ def test_lineno_in_error_message(self):
66
+ def assert_lineno(code):
67
+ e = code_error(code)
68
+ self.assertIn('line', e.format(include_complete_code=False))
69
+
70
+ assert_lineno(
71
+ """
72
+ x = y + 1
73
+ """
74
+ )
75
+ assert_lineno(
76
+ """
77
+ x = 1
78
+ y = 2
79
+ """
80
+ )
81
+ assert_lineno(
82
+ """
83
+ raise ValueError()
84
+ """
85
+ )
86
+
87
+
88
+ class SerializationErrorTest(unittest.TestCase):
89
+
90
+ def test_str(self):
91
+ e = errors.SerializationError(
92
+ 'Output cannot be serialized.', ValueError('abc'))
93
+ self.assertIn('Output cannot be serialized', str(e))
94
+ self.assertIn('ValueError: abc', str(e))
95
+
96
+
97
+ if __name__ == '__main__':
98
+ unittest.main()
@@ -0,0 +1,309 @@
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 execution."""
15
+
16
+ import ast
17
+ import contextlib
18
+ import io
19
+ import multiprocessing
20
+ import pickle
21
+ from typing import Any, Callable, Dict, Optional, Union
22
+
23
+ from pyglove.core import utils
24
+ from pyglove.core.coding import errors
25
+ from pyglove.core.coding import parsing
26
+ from pyglove.core.coding import permissions
27
+
28
+
29
+ # Key in returned dict that captures stdout.
30
+ STDOUT_KEY = '__stdout__'
31
+
32
+ # Key in the returned dict that represents the final result.
33
+ RESULT_KEY = '__result__'
34
+ _TLS_CODE_RUN_CONTEXT = '__code_run_context__'
35
+
36
+
37
+ @contextlib.contextmanager
38
+ def context(**kwargs):
39
+ """Context manager to inject symbols for code execution."""
40
+ ctx = get_context()
41
+ ctx.update(kwargs)
42
+ utils.thread_local_push(_TLS_CODE_RUN_CONTEXT, ctx)
43
+
44
+ try:
45
+ yield ctx
46
+ finally:
47
+ utils.thread_local_pop(_TLS_CODE_RUN_CONTEXT)
48
+
49
+
50
+ def get_context() -> Dict[str, Any]:
51
+ """Gets the current context for code execution."""
52
+ context_stack = utils.thread_local_get(_TLS_CODE_RUN_CONTEXT, None)
53
+ return dict(context_stack[-1]) if context_stack else {}
54
+
55
+
56
+ def evaluate(
57
+ code: str,
58
+ *,
59
+ global_vars: Optional[Dict[str, Any]] = None,
60
+ permission: Optional[permissions.CodePermission] = None,
61
+ returns_stdout: bool = False,
62
+ outputs_intermediate: bool = False,
63
+ ) -> Union[Any, Dict[str, Any]]:
64
+ """Executes Python code.
65
+
66
+ Features:
67
+ * Fine-grained execution policy for limiting what APIs could be executed.
68
+ This eliminates the need for sandboxing.
69
+ * It exposes both the final results and intermediate results (variables).
70
+
71
+ Args:
72
+ code: Python code to run.
73
+ global_vars: An optional dict as the globals that could be referenced by the
74
+ code.
75
+ permission: Permission for the Python code to run.
76
+ returns_stdout: If True, the stdout (a str) will be returned.
77
+ outputs_intermediate: Applicable when returns_stdout is False. If True,
78
+ intermediate output will be outputted as a dict, with the last line's
79
+ value accessible by key '__result__' and the std output accessible by
80
+ key '__stdout__'. Otherwise the value of the last line will be returned.
81
+
82
+ Returns:
83
+ The value of the last line of the code block. Or a dict of variable
84
+ names of all locals to their evaluated values as the output of the code to
85
+ run. The value for the last line can be accessed by key '__result__'. Or the
86
+ stdout as a str.
87
+ """
88
+ # Set up the permission and context.
89
+ permission = permission or permissions.get_permission()
90
+ ctx = dict(get_context())
91
+ if global_vars:
92
+ ctx.update(global_vars)
93
+
94
+ # Parse the code str.
95
+ code_block = parsing.parse(code, permission)
96
+ global_vars, orig_global_vars = ctx, ctx.copy()
97
+
98
+ # No code.
99
+ if not code_block.body: # pytype: disable=attribute-error
100
+ return {} if outputs_intermediate else None
101
+
102
+ stdout = io.StringIO()
103
+ with contextlib.redirect_stdout(stdout):
104
+ if hasattr(code_block.body[-1], 'value'): # pytype: disable=attribute-error
105
+ last_expr = code_block.body.pop() # pytype: disable=attribute-error
106
+ result_vars = [RESULT_KEY]
107
+
108
+ if isinstance(last_expr, ast.Assign):
109
+ for name_node in last_expr.targets:
110
+ result_vars.append(name_node.id)
111
+
112
+ last_expr = ast.Expression(last_expr.value) # pytype: disable=attribute-error
113
+
114
+ try:
115
+ # Execute the lines before the last expression.
116
+ # NOTE(daiyip): Only a `globals` dict is specified here, which will also
117
+ # be used to output intermediate values by `exec`. We do not specify a
118
+ # separate `locals` dict here, for - "If exec gets two separate objects
119
+ # as globals and locals, the code will be executed as if it were
120
+ # embedded in a class definition." - as the Python document explains.
121
+ # The outcome is that new functions defined in the code block could not
122
+ # be called by other newly defined functions.
123
+ # Refer to https://stackoverflow.com/questions/
124
+ # 73940751/why-cant-i-call-a-function-from-another-function-using-exec
125
+ # for more details.
126
+ exec(compile(code_block, '', mode='exec'), global_vars) # pylint: disable=exec-used
127
+
128
+ # Evaluate the last expression.
129
+ result = eval( # pylint: disable=eval-used
130
+ compile(last_expr, '', mode='eval'), global_vars
131
+ )
132
+ except Exception as e:
133
+ raise errors.CodeError(code, e) from e
134
+
135
+ for result_var in result_vars:
136
+ global_vars[result_var] = result
137
+ else:
138
+ try:
139
+ exec(compile(code_block, '', mode='exec'), global_vars) # pylint: disable=exec-used
140
+ except Exception as e:
141
+ raise errors.CodeError(code, e) from e
142
+ global_vars[RESULT_KEY] = list(global_vars.values())[-1]
143
+
144
+ if returns_stdout:
145
+ return stdout.getvalue()
146
+ if outputs_intermediate:
147
+ outputs = {}
148
+ for k, v in global_vars.items():
149
+ if k == '__builtins__':
150
+ continue
151
+ if k not in orig_global_vars or v is not orig_global_vars[k]:
152
+ outputs[k] = v
153
+ # Add stdout to outputs.
154
+ outputs[STDOUT_KEY] = stdout.getvalue()
155
+ return outputs
156
+ return global_vars[RESULT_KEY]
157
+
158
+
159
+ def sandbox_call(
160
+ func: Callable[..., Any],
161
+ *args,
162
+ timeout: Optional[float] = None,
163
+ **kwargs) -> Any:
164
+ """Calls a function with sandboxing.
165
+
166
+ Args:
167
+ func: Function to call.
168
+ *args: Positional arguments for `func`
169
+ timeout: Execution timeout in seconds. If None, wait `func` to complete.
170
+ **kwargs: Keyword arguments for `func`.
171
+
172
+ Returns:
173
+ Return value from `func`.
174
+
175
+ Raises:
176
+ TimeoutError: If the execution time exceeds the timeout.
177
+ Exception: Exception raised from `func`.
178
+ """
179
+ def _call(q, *args, **kwargs):
180
+ # NOTE(daiyip): if `q` is closed by the main process when `q.put` is called
181
+ # on a subprocess, ValueError will be raised. This is okay since the main
182
+ # process is no longer waiting for the result, and the subprocess could
183
+ # recycled with non-zero error code, which does not affect the main
184
+ # process.
185
+ def _run():
186
+ r = func(*args, **kwargs)
187
+ try:
188
+ return pickle.dumps(r)
189
+ except Exception as e:
190
+ raise errors.SerializationError(
191
+ f'Cannot serialize sandbox result: {r}', e
192
+ ) from e
193
+
194
+ try:
195
+ q.put(_run())
196
+ except Exception as e: # pylint: disable=broad-exception-caught
197
+ q.put(e)
198
+
199
+ q = multiprocessing.Queue()
200
+ try:
201
+ p = multiprocessing.Process(
202
+ target=_call, args=tuple([q] + list(args)), kwargs=kwargs)
203
+ p.start()
204
+ p.join(timeout=timeout)
205
+ if p.is_alive():
206
+ # We use `kill` instead of `terminate` to release process resources
207
+ # right away.
208
+ p.kill()
209
+ raise TimeoutError(f'Execution time exceed {timeout} seconds.')
210
+ x = q.get()
211
+ if isinstance(x, Exception):
212
+ raise x
213
+ try:
214
+ return pickle.loads(x)
215
+ except Exception as e:
216
+ raise errors.SerializationError(
217
+ 'Cannot deserialize the output from sandbox.', e
218
+ ) from e
219
+ finally:
220
+ q.close()
221
+
222
+
223
+ def maybe_sandbox_call(
224
+ func: Callable[..., Any],
225
+ *args,
226
+ sandbox: Optional[bool] = None,
227
+ timeout: Optional[float] = None,
228
+ **kwargs
229
+ ) -> Any:
230
+ """Maybe calls a function with sandboxing.
231
+
232
+ Args:
233
+ func: Function to call.
234
+ *args: Postional args that will be passed to `func`.
235
+ sandbox: If True, run code in sandbox; If False, run code in current
236
+ process. If None, run in sandbox first, if the output could not be
237
+ serialized and pass to current process, run the code again in current
238
+ process.
239
+ timeout: Execution timeout in seconds. If None, wait the code the complete.
240
+ **kwargs: Keyword args that will be passed to `func`.
241
+
242
+ Returns:
243
+ The return value of `func`.
244
+
245
+ Raises:
246
+ TimeoutError: If the execution time exceeds the timeout.
247
+ Exception: Exception that are raised from `func`.
248
+ """
249
+ if sandbox is None:
250
+ try:
251
+ return sandbox_call(func, *args, timeout=timeout, **kwargs)
252
+ # NOTE(daiyip): output could be serialized across processes, giving it
253
+ # already finishes on sandbox, so it should be much safer to run under
254
+ # current process.
255
+ except errors.SerializationError:
256
+ return func(*args, **kwargs)
257
+ elif sandbox:
258
+ return sandbox_call(func, *args, timeout=timeout, **kwargs)
259
+ else:
260
+ return func(*args, **kwargs)
261
+
262
+
263
+ def run(
264
+ code: str,
265
+ *,
266
+ global_vars: Optional[Dict[str, Any]] = None,
267
+ permission: Optional[permissions.CodePermission] = None,
268
+ returns_stdout: bool = False,
269
+ outputs_intermediate: bool = False,
270
+ sandbox: Optional[bool] = None,
271
+ timeout: Optional[float] = None,
272
+ ) -> Union[Any, Dict[str, Any]]:
273
+ """Executes Python code.
274
+
275
+ Features:
276
+ * Fine-grained execution policy for limiting what APIs could be executed.
277
+ This eliminates the need for sandboxing.
278
+ * It exposes both the final results and intermediate results (variables).
279
+
280
+ Args:
281
+ code: Python code to run.
282
+ global_vars: An optional dict of
283
+ permission: Permission for the Python code to run.
284
+ returns_stdout: If True, the stdout (a str) will be returned.
285
+ outputs_intermediate: Applicable when returns_stdout is False. If True,
286
+ intermediate output will be outputted as a dict, with the last line's
287
+ value accessible by key '__result__' and the std output accessible by
288
+ key '__stdout__'. Otherwise the value of the last line will be returned.
289
+ sandbox: If True, run code in sandbox; If False, run code in current
290
+ process. If None, run in sandbox first, if the output could not be
291
+ serialized and pass to current process, run the code again in current
292
+ process.
293
+ timeout: Execution timeout in seconds. If None, wait the code the complete.
294
+
295
+ Returns:
296
+ The value of the last line of the code block. Or a dict of variable
297
+ names of all locals to their evaluated values as the output of the code to
298
+ run. The value for the last line can be accessed by key '__result__'. Or the
299
+ stdout as a str.
300
+
301
+ Raises:
302
+ TimeoutError: If the execution time exceeds the timeout.
303
+ Exception: Exception that are raised from the code.
304
+ """
305
+ return maybe_sandbox_call(
306
+ evaluate, code=code, global_vars=global_vars, permission=permission,
307
+ returns_stdout=returns_stdout, outputs_intermediate=outputs_intermediate,
308
+ sandbox=sandbox, timeout=timeout
309
+ )