py-adtools 0.1.0__py3-none-any.whl → 0.1.1__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.

Potentially problematic release.


This version of py-adtools might be problematic. Click here for more details.

adtools/__init__.py CHANGED
@@ -1 +1,2 @@
1
1
  from .py_code import PyScript, PyFunction, PyClass, PyProgram
2
+ from .evaluator import PyEvaluator
adtools/evaluator.py ADDED
@@ -0,0 +1,178 @@
1
+ import multiprocessing
2
+ import os
3
+ import sys
4
+ import time
5
+ from abc import ABC, abstractmethod
6
+ from queue import Empty
7
+ from typing import Any, Literal, Dict, Callable, List
8
+ import psutil
9
+
10
+ from .py_code import PyProgram
11
+
12
+
13
+ class PyEvaluator(ABC):
14
+ """Python programs evaluator."""
15
+
16
+ def __init__(self, debug_mode: bool = False, *, exec_code: bool = True):
17
+ """Evaluator interface for evaluating the python algorithm program.
18
+ Args:
19
+ debug_mode : Debug mode.
20
+ exec_code : Using 'exec()' to compile the code and provide the callable function.
21
+ """
22
+ self._debug_mode = debug_mode
23
+ self._exec_code = exec_code
24
+ self._JOIN_TIMEOUT_SECONDS = 5
25
+
26
+ @abstractmethod
27
+ def evaluate_program(
28
+ self,
29
+ program_str: str,
30
+ callable_functions_dict: Dict[str, Callable] | None,
31
+ callable_functions_list: List[Callable] | None,
32
+ callable_classes_dict: Dict[str, Callable] | None,
33
+ callable_classes_list: List[Callable] | None,
34
+ **kwargs
35
+ ) -> Any | None:
36
+ """Evaluate a given program.
37
+ Args:
38
+ program_str : The raw program text.
39
+ callable_functions_dict: A dict maps function name to callable function.
40
+ callable_functions_list: A list of callable functions.
41
+ callable_classes_dict : A dict maps class name to callable class.
42
+ callable_classes_list : A list of callable classes.
43
+ Return:
44
+ Returns the evaluation result.
45
+ """
46
+ raise NotImplementedError('Must provide an evaluator for a python program. '
47
+ 'Override this method in a subclass.')
48
+
49
+ def _kill_process_and_its_children(self, process: multiprocessing.Process):
50
+ # Find all children processes
51
+ try:
52
+ parent = psutil.Process(process.pid)
53
+ children_processes = parent.children(recursive=True)
54
+ except psutil.NoSuchProcess:
55
+ children_processes = []
56
+ # Terminate parent process
57
+ process.terminate()
58
+ process.join(timeout=self._JOIN_TIMEOUT_SECONDS)
59
+ if process.is_alive():
60
+ process.kill()
61
+ process.join()
62
+ # Kill all children processes
63
+ for child in children_processes:
64
+ if self._debug_mode:
65
+ print(f"Killing process {process.pid}'s children process {child.pid}")
66
+ child.terminate()
67
+
68
+ def evaluate(self, program_str: str, **kwargs):
69
+ try:
70
+ # Parse to program instance
71
+ program = PyProgram.from_text(program_str)
72
+ function_names = [f.name for f in program.functions]
73
+ class_names = [c.name for c in program.classes]
74
+ if self._exec_code:
75
+ # Compile the program, and maps the global func/var/class name to its address
76
+ all_globals_namespace = {}
77
+ # Execute the program, map func/var/class to global namespace
78
+ exec(program_str, all_globals_namespace)
79
+ # Get callable functions
80
+ callable_functions_list = [all_globals_namespace[f_name] for f_name in function_names]
81
+ callable_functions_dict = dict(zip(function_names, callable_functions_list))
82
+ # Get callable classes
83
+ callable_classes_list = [all_globals_namespace[c_name] for c_name in class_names]
84
+ callable_classes_dict = dict(zip(class_names, callable_classes_list))
85
+ else:
86
+ callable_functions_list = None
87
+ callable_functions_dict = None
88
+ callable_classes_list = None
89
+ callable_classes_dict = None
90
+
91
+ # Get evaluate result
92
+ res = self.evaluate_program(
93
+ program_str,
94
+ callable_functions_dict,
95
+ callable_functions_list,
96
+ callable_classes_dict,
97
+ callable_classes_list,
98
+ **kwargs
99
+ )
100
+ return res
101
+ except Exception as e:
102
+ if self._debug_mode:
103
+ print(e)
104
+ return None
105
+
106
+ def _evaluate_in_safe_process(
107
+ self,
108
+ program_str: str,
109
+ result_queue: multiprocessing.Queue,
110
+ redirect_to_devnull: bool,
111
+ **kwargs
112
+ ):
113
+ if redirect_to_devnull:
114
+ with open('/dev/null', 'w') as devnull:
115
+ os.dup2(devnull.fileno(), sys.stdout.fileno())
116
+ os.dup2(devnull.fileno(), sys.stderr.fileno())
117
+ res = self.evaluate(program_str, **kwargs)
118
+ result_queue.put(res)
119
+
120
+ def secure_evaluate(
121
+ self,
122
+ program: str | PyProgram,
123
+ timeout_seconds: int | float = None,
124
+ redirect_to_devnull: bool = True,
125
+ multiprocessing_start_method=Literal['auto', 'fork', 'spawn'],
126
+ **kwargs
127
+ ):
128
+ """
129
+ Args:
130
+ program: the program to be evaluated.
131
+ timeout_seconds: return 'None' if the execution time exceeds 'timeout_seconds'.
132
+ redirect_to_devnull: redirect any output to '/dev/null'.
133
+ multiprocessing_start_method: start a process using 'fork' or 'spawn'.
134
+ """
135
+ if multiprocessing_start_method == 'auto':
136
+ # Force MacOS and Linux use 'fork' to generate new process
137
+ if sys.platform.startswith('darwin') or sys.platform.startswith('linux'):
138
+ multiprocessing.set_start_method('fork', force=True)
139
+ elif multiprocessing_start_method == 'fork':
140
+ multiprocessing.set_start_method('fork', force=True)
141
+ else:
142
+ multiprocessing.set_start_method('spawn', force=True)
143
+
144
+ try:
145
+ # Start evaluation process
146
+ result_queue = multiprocessing.Queue()
147
+ process = multiprocessing.Process(
148
+ target=self._evaluate_in_safe_process,
149
+ args=(str(program), result_queue, redirect_to_devnull),
150
+ kwargs=kwargs,
151
+ )
152
+ process.start()
153
+
154
+ if timeout_seconds is not None:
155
+ try:
156
+ # Get the result in timeout seconds
157
+ result = result_queue.get(timeout=timeout_seconds)
158
+ # After getting the result, terminate/kill the process
159
+ self._kill_process_and_its_children(process)
160
+ except Empty:
161
+ # Timeout
162
+ if self._debug_mode:
163
+ print(f'DEBUG: the evaluation time exceeds {timeout_seconds}s.')
164
+ self._kill_process_and_its_children(process)
165
+ result = None
166
+ except Exception as e:
167
+ if self._debug_mode:
168
+ print(f'DEBUG: evaluation failed with exception:\n{e}')
169
+ self._kill_process_and_its_children(process)
170
+ result = None
171
+ else:
172
+ result = result_queue.get()
173
+ self._kill_process_and_its_children(process)
174
+ return result
175
+ except Exception as e:
176
+ if self._debug_mode:
177
+ print(e)
178
+ return None
adtools/py_code.py CHANGED
@@ -44,7 +44,7 @@ class PyFunction:
44
44
  # Here, we assume the indentation is always four spaces.
45
45
  new_line = '\n' if self.body else ''
46
46
  function += f' """{self.docstring}"""{new_line}'
47
- # self.body is already indented.
47
+ # The self.body is already indented.
48
48
  function += self.body + '\n\n'
49
49
  return function
50
50
 
@@ -109,7 +109,7 @@ class PyClass:
109
109
  # Ensure there aren't leading & trailing new lines in `body`
110
110
  if name == 'body':
111
111
  value = value.strip('\n')
112
- # ensure there aren't leading & trailing quotes in `docstring`
112
+ # Ensure there aren't leading & trailing quotes in `docstring`
113
113
  if name == 'docstring' and value is not None:
114
114
  if '"""' in value:
115
115
  value = value.strip()
@@ -189,7 +189,7 @@ class _ProgramVisitor(ast.NodeVisitor):
189
189
  if has_decorators:
190
190
  # Find the minimum line number and retain the code above
191
191
  decorator_start_line = min(decorator.lineno for decorator in node.decorator_list)
192
- decorator = '\n'.join(self._codelines[decorator_start_line - 1: node.lineno - 1])
192
+ decorator = '\n'.join(self._codelines[decorator_start_line - 1: node.lineno - 1]).strip()
193
193
  # Update script end line
194
194
  script_end_line = decorator_start_line - 1
195
195
  else:
@@ -262,10 +262,10 @@ class _ProgramVisitor(ast.NodeVisitor):
262
262
  if has_decorators:
263
263
  # Find the minimum line number and retain the code above
264
264
  decorator_start_line = min(decorator.lineno for decorator in item.decorator_list)
265
- # Dedent decorator code for 4 spaces
265
+ # Dedent decorator code
266
266
  decorator = []
267
267
  for line in range(decorator_start_line - 1, item.lineno - 1):
268
- dedented_decorator = self._codelines[line][4:]
268
+ dedented_decorator = self._codelines[line].strip()
269
269
  decorator.append(dedented_decorator)
270
270
  decorator = '\n'.join(decorator)
271
271
  else:
@@ -0,0 +1,211 @@
1
+ Metadata-Version: 2.4
2
+ Name: py-adtools
3
+ Version: 0.1.1
4
+ Summary: Useful tools for parsing Python programs for algorithm design.
5
+ Home-page: https://github.com/RayZhhh/py-adtools
6
+ Author: Rui Zhang
7
+ Author-email: rzhang.cs@gmail.com
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Operating System :: OS Independent
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Topic :: Scientific/Engineering
12
+ Requires-Python: >=3.10
13
+ Description-Content-Type: text/markdown
14
+ License-File: LICENSE
15
+ Dynamic: author
16
+ Dynamic: author-email
17
+ Dynamic: classifier
18
+ Dynamic: description
19
+ Dynamic: description-content-type
20
+ Dynamic: home-page
21
+ Dynamic: license-file
22
+ Dynamic: requires-python
23
+ Dynamic: summary
24
+
25
+ # Useful tools for parsing and evaluating Python programs for algorithm design
26
+
27
+ ------
28
+
29
+ > This repo aims to help develop more powerful [Large Language Models for Algorithm Design (LLM4AD)](https://github.com/Optima-CityU/llm4ad) applications.
30
+ >
31
+ > More tools will be provided soon.
32
+
33
+ ------
34
+
35
+ The figure demonstrates how a Python program is parsed into `PyScript`, `PyFunction`, `PyClass,` and `PyProgram` via `adtools`.
36
+
37
+ ![pycode](./assets/pycode.png)
38
+
39
+ ------
40
+
41
+ ## Installation
42
+
43
+ > [!TIP]
44
+ >
45
+ > It is recommended to use Python >= 3.10.
46
+
47
+ Run the following instructions to install adtools.
48
+
49
+ ```shell
50
+ pip install git+https://github.com/RayZhhh/adtool.git
51
+ ```
52
+
53
+ Or install via pip:
54
+
55
+ ```shell
56
+ pip install py-adtools
57
+ ```
58
+
59
+ ## Usage
60
+
61
+ ### Parser for a Python program
62
+
63
+ Parse your code (in string) into Python code instances, so that you can check each component and modify it.
64
+
65
+ ```python
66
+ from adtools import PyProgram
67
+
68
+ code = r'''
69
+ import ast, numba # This part will be parsed into PyScript
70
+ import numpy as np
71
+
72
+ @numba.jit() # This part will be parsed into PyFunction
73
+ def function(arg1, arg2=True):
74
+ if arg2:
75
+ return arg1 * 2
76
+ else:
77
+ return arg1 * 4
78
+
79
+ @some.decorators() # This part will be parsed into PyClass
80
+ class PythonClass(BaseClass):
81
+ class_var1 = 1 # This part will be parsed into PyScript
82
+ class_varb = 2 # and placed in PyClass.class_vars_and_code
83
+
84
+ def __init__(self, x): # This part will be parsed into PyFunction
85
+ self.x = x # and placed in PyClass.functions
86
+
87
+ def method1(self):
88
+ return self.x * 10
89
+
90
+ @some.decorators()
91
+ def method2(self, x, y):
92
+ return x + y + self.method1(x)
93
+
94
+ class InnerClass: # This part will be parsed into PyScript
95
+ def __init__(self): # and placed in PyClass.class_vars_and_code
96
+ ...
97
+
98
+ if __name__ == '__main__': # This part will be parsed into PyScript
99
+ res = function(1)
100
+ print(res)
101
+ res = PythonClass().method2(1, 2)
102
+ '''
103
+
104
+ p = PyProgram.from_text(code)
105
+ print(p)
106
+ print(f'-------------------------------------')
107
+ print(p.classes[0].functions[2].decorator)
108
+ print(f'-------------------------------------')
109
+ print(p.functions[0].name)
110
+ ```
111
+
112
+ ### Evaluate Python programs
113
+
114
+ Evaluate Python programs in a secure process to avoid the abortation of the main process. Two steps:
115
+
116
+ - Extend the `PyEvaluator` class and override the `evaluate_program` method.
117
+ - Evaluate the program (in str) by calling the `evaluate` (directly evaluate without executing in a sandbox process) or the `secure_evaluate` (evaluate in a sandbox process) methods.
118
+
119
+ ```python
120
+ import time
121
+ from typing import Dict, Callable, List, Any
122
+
123
+ from adtools import PyEvaluator
124
+
125
+
126
+ class SortAlgorithmEvaluator(PyEvaluator):
127
+ def evaluate_program(
128
+ self,
129
+ program_str: str,
130
+ callable_functions_dict: Dict[str, Callable] | None,
131
+ callable_functions_list: List[Callable] | None,
132
+ callable_classes_dict: Dict[str, Callable] | None,
133
+ callable_classes_list: List[Callable] | None,
134
+ **kwargs
135
+ ) -> Any | None:
136
+ """Evaluate a given sort algorithm program.
137
+ Args:
138
+ program_str : The raw program text.
139
+ callable_functions_dict: A dict maps function name to callable function.
140
+ callable_functions_list: A list of callable functions.
141
+ callable_classes_dict : A dict maps class name to callable class.
142
+ callable_classes_list : A list of callable classes.
143
+ Return:
144
+ Returns the evaluation result.
145
+ """
146
+ # Get the sort algorithm
147
+ sort_algo: Callable = callable_functions_dict['merge_sort']
148
+ # Test data
149
+ input = [10, 2, 4, 76, 19, 29, 3, 5, 1]
150
+ # Compute execution time
151
+ start = time.time()
152
+ res = sort_algo(input)
153
+ duration = time.time() - start
154
+ if res == sorted(input): # If the result is correct
155
+ return duration # Return the execution time as the score of the algorithm
156
+ else:
157
+ return None # Return None as the algorithm is incorrect
158
+
159
+
160
+ code_generated_by_llm = '''
161
+ def merge_sort(arr):
162
+ if len(arr) <= 1:
163
+ return arr
164
+
165
+ mid = len(arr) // 2
166
+ left = merge_sort(arr[:mid])
167
+ right = merge_sort(arr[mid:])
168
+
169
+ return merge(left, right)
170
+
171
+ def merge(left, right):
172
+ result = []
173
+ i = j = 0
174
+
175
+ while i < len(left) and j < len(right):
176
+ if left[i] < right[j]:
177
+ result.append(left[i])
178
+ i += 1
179
+ else:
180
+ result.append(right[j])
181
+ j += 1
182
+
183
+ result.extend(left[i:])
184
+ result.extend(right[j:])
185
+
186
+ return result
187
+ '''
188
+
189
+ harmful_code_generated_by_llm = '''
190
+ def merge_sort(arr):
191
+ while True:
192
+ pass
193
+ '''
194
+
195
+ if __name__ == '__main__':
196
+ evaluator = SortAlgorithmEvaluator()
197
+
198
+ # Evaluate
199
+ score = evaluator.evaluate(code_generated_by_llm)
200
+ print(f'Score: {score}')
201
+
202
+ # Secure evaluate (the evaluation is executed in a sandbox process)
203
+ score = evaluator.secure_evaluate(code_generated_by_llm, timeout_seconds=10)
204
+ print(f'Score: {score}')
205
+
206
+ # Evaluate a harmful code, the evaluation will be terminated within 10 seconds
207
+ # We will obtain a score of `None` due to the violation of time restriction
208
+ score = evaluator.secure_evaluate(harmful_code_generated_by_llm, timeout_seconds=10)
209
+ print(f'Score: {score}')
210
+ ```
211
+
@@ -0,0 +1,8 @@
1
+ adtools/__init__.py,sha256=99EPY13OhXs2Zt7IOsa53uQ0Uv-g9dSkHby8mEjDdQM,96
2
+ adtools/evaluator.py,sha256=KyYUqVhUpek1vS0CdiMJaNooVtIZVugIvzVH6bGi81Y,7255
3
+ adtools/py_code.py,sha256=1XNc5ckX_ivePSGYEEcKHXat-v4zjbxKUcYiTIEjqCw,13649
4
+ py_adtools-0.1.1.dist-info/licenses/LICENSE,sha256=E5GGyecx3y5h2gcEGQloF-rDY9wbaef5IHjRsvtFbt8,1065
5
+ py_adtools-0.1.1.dist-info/METADATA,sha256=FrDmTPVetLES_vx0q0n5v1nf8OXfFA2vvG3nChka2Vo,6168
6
+ py_adtools-0.1.1.dist-info/WHEEL,sha256=lTU6B6eIfYoiQJTZNc-fyaR6BpL6ehTzU3xGYxn2n8k,91
7
+ py_adtools-0.1.1.dist-info/top_level.txt,sha256=X2kKzmJFDAKR2FWCij5pfMG9pVVjVUomyl4e-1VLXIk,8
8
+ py_adtools-0.1.1.dist-info/RECORD,,
@@ -1,89 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: py-adtools
3
- Version: 0.1.0
4
- Summary: Useful tools for parsing Python programs for algorithm design.
5
- Home-page: https://github.com/RayZhhh/py-adtools
6
- Author: Rui Zhang
7
- Author-email: rzhang.cs@gmail.com
8
- Classifier: Programming Language :: Python :: 3
9
- Classifier: Operating System :: OS Independent
10
- Classifier: Intended Audience :: Developers
11
- Classifier: Topic :: Scientific/Engineering
12
- Requires-Python: >=3.10
13
- Description-Content-Type: text/markdown
14
- License-File: LICENSE
15
- Dynamic: author
16
- Dynamic: author-email
17
- Dynamic: classifier
18
- Dynamic: description
19
- Dynamic: description-content-type
20
- Dynamic: home-page
21
- Dynamic: license-file
22
- Dynamic: requires-python
23
- Dynamic: summary
24
-
25
- # Useful tools for parsing Python programs for algorithm design
26
-
27
- ------
28
-
29
- > This repo aims to help develop more powerful [Large Language Models for Algorithm Design (LLM4AD)](https://github.com/Optima-CityU/llm4ad) applications.
30
- >
31
- > More tools will be provided soon.
32
-
33
- ------
34
-
35
- The figure demonstrates how a Python program is parsed into `PyScript`, `PyFunction`, `PyClass,` and `PyProgram` via `adtools`.
36
-
37
- ![pycode](./assets/pycode.png)
38
-
39
- ------
40
-
41
- ## Installation
42
-
43
- > [!TIP]
44
- >
45
- > It is recommended to use Python >= 3.10.
46
-
47
- Run the following instructions to install adtools.
48
-
49
- ```shell
50
- pip install git+https://github.com/RayZhhh/adtool.git
51
- ```
52
-
53
- ## Usage
54
-
55
- Parse your code (in string) into Python code instances.
56
-
57
- ```python
58
- from adtools import PyProgram
59
-
60
- code = r'''
61
- import ast
62
- import numpy as np
63
-
64
- def func():
65
- a = 5
66
- return a + a
67
-
68
- class A(B):
69
- a=1
70
-
71
- @yes()
72
- @deco()
73
- def __init__(self):
74
- pass
75
-
76
- def method(self):
77
- pass
78
-
79
- b=2
80
- '''
81
-
82
- p = PyProgram.from_text(code)
83
- print(p)
84
- print(f'-------------------------------------')
85
- print(p.classes[0].functions[0].decorator)
86
- print(f'-------------------------------------')
87
- print(p.functions[0].name)
88
- ```
89
-
@@ -1,7 +0,0 @@
1
- adtools/__init__.py,sha256=l1JmS0mViywF9Nv3Dt4wtrOewfbTwchq_5vfiTwtMSw,62
2
- adtools/py_code.py,sha256=4_KGvhHZaruVFtZ3LTERkJ-swXgPWyTtEwIMTm39dwQ,13646
3
- py_adtools-0.1.0.dist-info/licenses/LICENSE,sha256=E5GGyecx3y5h2gcEGQloF-rDY9wbaef5IHjRsvtFbt8,1065
4
- py_adtools-0.1.0.dist-info/METADATA,sha256=1YEeHkE81yVH6vMGfXcjp9phR8j_uEyFdyf-IsKUlrQ,1870
5
- py_adtools-0.1.0.dist-info/WHEEL,sha256=lTU6B6eIfYoiQJTZNc-fyaR6BpL6ehTzU3xGYxn2n8k,91
6
- py_adtools-0.1.0.dist-info/top_level.txt,sha256=X2kKzmJFDAKR2FWCij5pfMG9pVVjVUomyl4e-1VLXIk,8
7
- py_adtools-0.1.0.dist-info/RECORD,,