onetick-py 1.177.0__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.
- locator_parser/__init__.py +0 -0
- locator_parser/acl.py +73 -0
- locator_parser/actions.py +262 -0
- locator_parser/common.py +368 -0
- locator_parser/io.py +43 -0
- locator_parser/locator.py +150 -0
- onetick/__init__.py +101 -0
- onetick/doc_utilities/__init__.py +3 -0
- onetick/doc_utilities/napoleon.py +40 -0
- onetick/doc_utilities/ot_doctest.py +140 -0
- onetick/doc_utilities/snippets.py +279 -0
- onetick/lib/__init__.py +4 -0
- onetick/lib/instance.py +141 -0
- onetick/py/__init__.py +293 -0
- onetick/py/_stack_info.py +89 -0
- onetick/py/_version.py +2 -0
- onetick/py/aggregations/__init__.py +11 -0
- onetick/py/aggregations/_base.py +648 -0
- onetick/py/aggregations/_docs.py +948 -0
- onetick/py/aggregations/compute.py +286 -0
- onetick/py/aggregations/functions.py +2216 -0
- onetick/py/aggregations/generic.py +104 -0
- onetick/py/aggregations/high_low.py +80 -0
- onetick/py/aggregations/num_distinct.py +83 -0
- onetick/py/aggregations/order_book.py +501 -0
- onetick/py/aggregations/other.py +1014 -0
- onetick/py/backports.py +26 -0
- onetick/py/cache.py +374 -0
- onetick/py/callback/__init__.py +5 -0
- onetick/py/callback/callback.py +276 -0
- onetick/py/callback/callbacks.py +131 -0
- onetick/py/compatibility.py +798 -0
- onetick/py/configuration.py +771 -0
- onetick/py/core/__init__.py +0 -0
- onetick/py/core/_csv_inspector.py +93 -0
- onetick/py/core/_internal/__init__.py +0 -0
- onetick/py/core/_internal/_manually_bound_value.py +6 -0
- onetick/py/core/_internal/_nodes_history.py +250 -0
- onetick/py/core/_internal/_op_utils/__init__.py +0 -0
- onetick/py/core/_internal/_op_utils/every_operand.py +9 -0
- onetick/py/core/_internal/_op_utils/is_const.py +10 -0
- onetick/py/core/_internal/_per_tick_scripts/tick_list_sort_template.script +121 -0
- onetick/py/core/_internal/_proxy_node.py +140 -0
- onetick/py/core/_internal/_state_objects.py +2312 -0
- onetick/py/core/_internal/_state_vars.py +93 -0
- onetick/py/core/_source/__init__.py +0 -0
- onetick/py/core/_source/_symbol_param.py +95 -0
- onetick/py/core/_source/schema.py +97 -0
- onetick/py/core/_source/source_methods/__init__.py +0 -0
- onetick/py/core/_source/source_methods/aggregations.py +809 -0
- onetick/py/core/_source/source_methods/applyers.py +296 -0
- onetick/py/core/_source/source_methods/columns.py +141 -0
- onetick/py/core/_source/source_methods/data_quality.py +301 -0
- onetick/py/core/_source/source_methods/debugs.py +272 -0
- onetick/py/core/_source/source_methods/drops.py +120 -0
- onetick/py/core/_source/source_methods/fields.py +619 -0
- onetick/py/core/_source/source_methods/filters.py +1002 -0
- onetick/py/core/_source/source_methods/joins.py +1413 -0
- onetick/py/core/_source/source_methods/merges.py +605 -0
- onetick/py/core/_source/source_methods/misc.py +1455 -0
- onetick/py/core/_source/source_methods/pandases.py +155 -0
- onetick/py/core/_source/source_methods/renames.py +356 -0
- onetick/py/core/_source/source_methods/sorts.py +183 -0
- onetick/py/core/_source/source_methods/switches.py +142 -0
- onetick/py/core/_source/source_methods/symbols.py +117 -0
- onetick/py/core/_source/source_methods/times.py +627 -0
- onetick/py/core/_source/source_methods/writes.py +986 -0
- onetick/py/core/_source/symbol.py +205 -0
- onetick/py/core/_source/tmp_otq.py +222 -0
- onetick/py/core/column.py +209 -0
- onetick/py/core/column_operations/__init__.py +0 -0
- onetick/py/core/column_operations/_methods/__init__.py +4 -0
- onetick/py/core/column_operations/_methods/_internal.py +28 -0
- onetick/py/core/column_operations/_methods/conversions.py +216 -0
- onetick/py/core/column_operations/_methods/methods.py +292 -0
- onetick/py/core/column_operations/_methods/op_types.py +160 -0
- onetick/py/core/column_operations/accessors/__init__.py +0 -0
- onetick/py/core/column_operations/accessors/_accessor.py +28 -0
- onetick/py/core/column_operations/accessors/decimal_accessor.py +104 -0
- onetick/py/core/column_operations/accessors/dt_accessor.py +537 -0
- onetick/py/core/column_operations/accessors/float_accessor.py +184 -0
- onetick/py/core/column_operations/accessors/str_accessor.py +1367 -0
- onetick/py/core/column_operations/base.py +1121 -0
- onetick/py/core/cut_builder.py +150 -0
- onetick/py/core/db_constants.py +20 -0
- onetick/py/core/eval_query.py +245 -0
- onetick/py/core/lambda_object.py +441 -0
- onetick/py/core/multi_output_source.py +232 -0
- onetick/py/core/per_tick_script.py +2256 -0
- onetick/py/core/query_inspector.py +464 -0
- onetick/py/core/source.py +1744 -0
- onetick/py/db/__init__.py +2 -0
- onetick/py/db/_inspection.py +1128 -0
- onetick/py/db/db.py +1327 -0
- onetick/py/db/utils.py +64 -0
- onetick/py/docs/__init__.py +0 -0
- onetick/py/docs/docstring_parser.py +112 -0
- onetick/py/docs/utils.py +81 -0
- onetick/py/functions.py +2398 -0
- onetick/py/license.py +190 -0
- onetick/py/log.py +88 -0
- onetick/py/math.py +935 -0
- onetick/py/misc.py +470 -0
- onetick/py/oqd/__init__.py +22 -0
- onetick/py/oqd/eps.py +1195 -0
- onetick/py/oqd/sources.py +325 -0
- onetick/py/otq.py +216 -0
- onetick/py/pyomd_mock.py +47 -0
- onetick/py/run.py +916 -0
- onetick/py/servers.py +173 -0
- onetick/py/session.py +1347 -0
- onetick/py/sources/__init__.py +19 -0
- onetick/py/sources/cache.py +167 -0
- onetick/py/sources/common.py +128 -0
- onetick/py/sources/csv.py +642 -0
- onetick/py/sources/custom.py +85 -0
- onetick/py/sources/data_file.py +305 -0
- onetick/py/sources/data_source.py +1045 -0
- onetick/py/sources/empty.py +94 -0
- onetick/py/sources/odbc.py +337 -0
- onetick/py/sources/order_book.py +271 -0
- onetick/py/sources/parquet.py +168 -0
- onetick/py/sources/pit.py +191 -0
- onetick/py/sources/query.py +495 -0
- onetick/py/sources/snapshots.py +419 -0
- onetick/py/sources/split_query_output_by_symbol.py +198 -0
- onetick/py/sources/symbology_mapping.py +123 -0
- onetick/py/sources/symbols.py +374 -0
- onetick/py/sources/ticks.py +825 -0
- onetick/py/sql.py +70 -0
- onetick/py/state.py +251 -0
- onetick/py/types.py +2131 -0
- onetick/py/utils/__init__.py +70 -0
- onetick/py/utils/acl.py +93 -0
- onetick/py/utils/config.py +186 -0
- onetick/py/utils/default.py +49 -0
- onetick/py/utils/file.py +38 -0
- onetick/py/utils/helpers.py +76 -0
- onetick/py/utils/locator.py +94 -0
- onetick/py/utils/perf.py +498 -0
- onetick/py/utils/query.py +49 -0
- onetick/py/utils/render.py +1374 -0
- onetick/py/utils/script.py +244 -0
- onetick/py/utils/temp.py +471 -0
- onetick/py/utils/types.py +120 -0
- onetick/py/utils/tz.py +84 -0
- onetick_py-1.177.0.dist-info/METADATA +137 -0
- onetick_py-1.177.0.dist-info/RECORD +152 -0
- onetick_py-1.177.0.dist-info/WHEEL +5 -0
- onetick_py-1.177.0.dist-info/entry_points.txt +2 -0
- onetick_py-1.177.0.dist-info/licenses/LICENSE +21 -0
- onetick_py-1.177.0.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1,2256 @@
|
|
|
1
|
+
import ast
|
|
2
|
+
import inspect
|
|
3
|
+
import textwrap
|
|
4
|
+
import types
|
|
5
|
+
import operator
|
|
6
|
+
import tokenize
|
|
7
|
+
import warnings
|
|
8
|
+
from typing import Callable, Union, Any, Optional, Iterable, Type, Tuple, Dict, List, TypeVar
|
|
9
|
+
from copy import deepcopy
|
|
10
|
+
from functools import wraps
|
|
11
|
+
|
|
12
|
+
from onetick.py.backports import astunparse, cached_property
|
|
13
|
+
|
|
14
|
+
from collections import deque
|
|
15
|
+
from contextlib import contextmanager
|
|
16
|
+
|
|
17
|
+
from .. import types as ott
|
|
18
|
+
from .column_operations.base import _Operation
|
|
19
|
+
from .column import _Column
|
|
20
|
+
from .lambda_object import _EmulateObject, _LambdaIfElse, _EmulateStateVars, _CompareTrackScope
|
|
21
|
+
from ._internal._state_objects import (
|
|
22
|
+
_TickSequence, _TickSequenceTickBase, _TickListTick, _TickSetTick, _TickDequeTick, _DynamicTick
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Static:
|
|
27
|
+
"""
|
|
28
|
+
Class for declaring static local variable in per-tick script.
|
|
29
|
+
Static variables are defined once and save their values between
|
|
30
|
+
arrival of the input ticks.
|
|
31
|
+
"""
|
|
32
|
+
def __init__(self, value):
|
|
33
|
+
self.value = value
|
|
34
|
+
|
|
35
|
+
# these functions needed mostly for linters
|
|
36
|
+
def __getattr__(self, item):
|
|
37
|
+
return self.value.__getattr__(item)
|
|
38
|
+
|
|
39
|
+
def __getitem__(self, item):
|
|
40
|
+
return operator.getitem(self.value, item)
|
|
41
|
+
|
|
42
|
+
def __setitem__(self, key, value):
|
|
43
|
+
return operator.setitem(self.value, key, value)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class LocalVariable(_Operation):
|
|
47
|
+
"""
|
|
48
|
+
Class for inner representation of local variable in per-tick script.
|
|
49
|
+
Only simple values are supported, tick sequences are represented by another class.
|
|
50
|
+
"""
|
|
51
|
+
def __init__(self, name, dtype=None):
|
|
52
|
+
super().__init__(op_str=f'LOCAL::{name}', dtype=dtype)
|
|
53
|
+
self.name = name
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class TickDescriptorFields(_TickSequence):
|
|
57
|
+
"""
|
|
58
|
+
Class for declaring tick descriptor fields in per-tick script.
|
|
59
|
+
Can only be iterated, doesn't have methods and parameters.
|
|
60
|
+
|
|
61
|
+
See also
|
|
62
|
+
--------
|
|
63
|
+
:py:class:`TickDescriptorField <onetick.py.core.per_tick_script.TickDescriptorField>`
|
|
64
|
+
|
|
65
|
+
Examples
|
|
66
|
+
--------
|
|
67
|
+
>>> t = otp.Tick(A=1)
|
|
68
|
+
>>> def fun(tick):
|
|
69
|
+
... for field in otp.tick_descriptor_fields():
|
|
70
|
+
... tick['NAME'] = field.get_name()
|
|
71
|
+
>>> t = t.script(fun)
|
|
72
|
+
>>> otp.run(t)
|
|
73
|
+
Time A NAME
|
|
74
|
+
0 2003-12-01 1 A
|
|
75
|
+
"""
|
|
76
|
+
def __init__(self):
|
|
77
|
+
# we don't want to inherit parent class init
|
|
78
|
+
pass
|
|
79
|
+
|
|
80
|
+
def __str__(self):
|
|
81
|
+
return 'LOCAL::INPUT_TICK_DESCRIPTOR_FIELDS'
|
|
82
|
+
|
|
83
|
+
@property
|
|
84
|
+
def _tick_class(self):
|
|
85
|
+
return TickDescriptorField
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def tick_list_tick():
|
|
89
|
+
"""
|
|
90
|
+
Can be used only in per-tick script function
|
|
91
|
+
to define a tick list tick local variable.
|
|
92
|
+
|
|
93
|
+
Tick list ticks can be used with some methods
|
|
94
|
+
of tick lists :py:class:`onetick.py.state.tick_list`.
|
|
95
|
+
|
|
96
|
+
See also
|
|
97
|
+
--------
|
|
98
|
+
:py:class:`onetick.py.state.tick_list`.
|
|
99
|
+
|
|
100
|
+
Returns
|
|
101
|
+
-------
|
|
102
|
+
:py:class:`onetick.py.static` value with tick object.
|
|
103
|
+
|
|
104
|
+
Examples
|
|
105
|
+
--------
|
|
106
|
+
>>> def fun(tick):
|
|
107
|
+
... t = otp.tick_list_tick()
|
|
108
|
+
... tick.state_vars['LIST'].push_back(t)
|
|
109
|
+
"""
|
|
110
|
+
return Static(_TickListTick(None))
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def tick_set_tick():
|
|
114
|
+
"""
|
|
115
|
+
Can be used only in per-tick script function
|
|
116
|
+
to define a tick set tick local variable.
|
|
117
|
+
|
|
118
|
+
Tick set ticks can be used with some methods
|
|
119
|
+
of tick sets :py:class:`onetick.py.state.tick_set`.
|
|
120
|
+
|
|
121
|
+
See also
|
|
122
|
+
--------
|
|
123
|
+
:py:class:`onetick.py.state.tick_set`.
|
|
124
|
+
|
|
125
|
+
Returns
|
|
126
|
+
-------
|
|
127
|
+
:py:class:`onetick.py.static` value with tick object.
|
|
128
|
+
|
|
129
|
+
Examples
|
|
130
|
+
--------
|
|
131
|
+
>>> def fun(tick):
|
|
132
|
+
... t = otp.tick_set_tick()
|
|
133
|
+
... if tick.state_vars['SET'].find(t, -1):
|
|
134
|
+
... tick['RES'] = '-1'
|
|
135
|
+
"""
|
|
136
|
+
return Static(_TickSetTick(None))
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def tick_deque_tick():
|
|
140
|
+
"""
|
|
141
|
+
Can be used only in per-tick script function
|
|
142
|
+
to define a tick deque tick local variable.
|
|
143
|
+
|
|
144
|
+
Tick deque ticks can be used with some methods
|
|
145
|
+
of tick deques :py:class:`onetick.py.state.tick_deque`.
|
|
146
|
+
|
|
147
|
+
See also
|
|
148
|
+
--------
|
|
149
|
+
:py:class:`onetick.py.state.tick_deque`.
|
|
150
|
+
|
|
151
|
+
Returns
|
|
152
|
+
-------
|
|
153
|
+
:py:class:`onetick.py.static` value with tick object.
|
|
154
|
+
|
|
155
|
+
Examples
|
|
156
|
+
--------
|
|
157
|
+
>>> def fun(tick):
|
|
158
|
+
... t = otp.tick_deque_tick()
|
|
159
|
+
... tick.state_vars['DEQUE'].get_tick(0, t)
|
|
160
|
+
"""
|
|
161
|
+
return Static(_TickDequeTick(None))
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def dynamic_tick():
|
|
165
|
+
"""
|
|
166
|
+
Can be used only in per-tick script function
|
|
167
|
+
to define a dynamic tick local variable.
|
|
168
|
+
|
|
169
|
+
Dynamic ticks can be used with some methods
|
|
170
|
+
of all tick sequences.
|
|
171
|
+
|
|
172
|
+
See also
|
|
173
|
+
--------
|
|
174
|
+
:py:class:`onetick.py.state.tick_list`
|
|
175
|
+
:py:class:`onetick.py.state.tick_set`
|
|
176
|
+
:py:class:`onetick.py.state.tick_deque`
|
|
177
|
+
|
|
178
|
+
Returns
|
|
179
|
+
-------
|
|
180
|
+
:py:class:`onetick.py.static` value with tick object.
|
|
181
|
+
|
|
182
|
+
Examples
|
|
183
|
+
--------
|
|
184
|
+
>>> def fun(tick):
|
|
185
|
+
... t = otp.dynamic_tick()
|
|
186
|
+
... t['X'] = tick['SUM']
|
|
187
|
+
"""
|
|
188
|
+
return Static(_DynamicTick(None))
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
class TickDescriptorField(_TickSequenceTickBase):
|
|
192
|
+
"""
|
|
193
|
+
Tick descriptor field object.
|
|
194
|
+
Can be accessed only while iterating over
|
|
195
|
+
:py:class:`otp.tick_descriptor_fields <onetick.py.core.per_tick_script.TickDescriptorFields>`
|
|
196
|
+
in per-tick script.
|
|
197
|
+
|
|
198
|
+
Examples
|
|
199
|
+
--------
|
|
200
|
+
>>> t = otp.Tick(A=2, B='B', C=1.2345)
|
|
201
|
+
>>> def fun(tick):
|
|
202
|
+
... tick['NAMES'] = ''
|
|
203
|
+
... tick['TYPES'] = ''
|
|
204
|
+
... tick['SIZES'] = ''
|
|
205
|
+
... for field in otp.tick_descriptor_fields():
|
|
206
|
+
... tick['NAMES'] += field.get_name() + ','
|
|
207
|
+
... tick['TYPES'] += field.get_type() + ','
|
|
208
|
+
... tick['SIZES'] += field.get_size().apply(str) + ','
|
|
209
|
+
>>> t = t.script(fun)
|
|
210
|
+
>>> otp.run(t)
|
|
211
|
+
Time A B C NAMES TYPES SIZES
|
|
212
|
+
0 2003-12-01 2 B 1.2345 A,B,C, long,string,double, 8,64,8,
|
|
213
|
+
"""
|
|
214
|
+
|
|
215
|
+
_definition = 'TICK_DESCRIPTOR_FIELD'
|
|
216
|
+
|
|
217
|
+
def get_field_name(self):
|
|
218
|
+
"""
|
|
219
|
+
Get the name of the field.
|
|
220
|
+
|
|
221
|
+
Returns
|
|
222
|
+
-------
|
|
223
|
+
onetick.py.Operation
|
|
224
|
+
"""
|
|
225
|
+
return _Operation(op_str=f'{self}.GET_FIELD_NAME()', dtype=str)
|
|
226
|
+
|
|
227
|
+
def get_name(self):
|
|
228
|
+
"""
|
|
229
|
+
Get the name of the field.
|
|
230
|
+
|
|
231
|
+
Returns
|
|
232
|
+
-------
|
|
233
|
+
onetick.py.Operation
|
|
234
|
+
"""
|
|
235
|
+
return self.get_field_name()
|
|
236
|
+
|
|
237
|
+
def get_size(self):
|
|
238
|
+
"""
|
|
239
|
+
Get the size of the type of the field.
|
|
240
|
+
|
|
241
|
+
Returns
|
|
242
|
+
-------
|
|
243
|
+
onetick.py.Operation
|
|
244
|
+
"""
|
|
245
|
+
return _Operation(op_str=f'{self}.GET_SIZE()', dtype=int)
|
|
246
|
+
|
|
247
|
+
def get_type(self):
|
|
248
|
+
"""
|
|
249
|
+
Get the name of the type of the field.
|
|
250
|
+
|
|
251
|
+
Returns
|
|
252
|
+
-------
|
|
253
|
+
onetick.py.Operation
|
|
254
|
+
"""
|
|
255
|
+
return _Operation(op_str=f'{self}.GET_TYPE()', dtype=str)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
class Expression:
|
|
259
|
+
"""
|
|
260
|
+
Class to save per-tick-script expressions along with their possible values.
|
|
261
|
+
|
|
262
|
+
Parameters
|
|
263
|
+
----------
|
|
264
|
+
expr
|
|
265
|
+
string expression that will be saved to per tick script
|
|
266
|
+
values:
|
|
267
|
+
values that this expression can take.
|
|
268
|
+
For example, bool operation can take many values.
|
|
269
|
+
lhs:
|
|
270
|
+
True if expression is left hand expression.
|
|
271
|
+
In this case value of expression must be callable.
|
|
272
|
+
Calling it with right hand expression value as an argument
|
|
273
|
+
should be the same as execute the whole expression.
|
|
274
|
+
"""
|
|
275
|
+
def __init__(self, *values: Any, expr: Optional[str] = None, lhs: bool = False):
|
|
276
|
+
self.values = values
|
|
277
|
+
self._expr = expr
|
|
278
|
+
self.lhs = lhs
|
|
279
|
+
if self.lhs:
|
|
280
|
+
assert callable(self.value)
|
|
281
|
+
assert self.expr
|
|
282
|
+
|
|
283
|
+
@property
|
|
284
|
+
def expr(self):
|
|
285
|
+
if self._expr:
|
|
286
|
+
return self._expr
|
|
287
|
+
if self.is_emulator:
|
|
288
|
+
self._expr = 'LOCAL::OUTPUT_TICK'
|
|
289
|
+
elif self.is_column:
|
|
290
|
+
self._expr = str(self.value)
|
|
291
|
+
elif self.values:
|
|
292
|
+
self._expr = self.value_to_onetick(self.value)
|
|
293
|
+
return self._expr
|
|
294
|
+
|
|
295
|
+
@property
|
|
296
|
+
def value(self):
|
|
297
|
+
length = len(self.values)
|
|
298
|
+
if length == 0:
|
|
299
|
+
raise ValueError(f"Expression '{self}' doesn't have values.")
|
|
300
|
+
if length > 1:
|
|
301
|
+
raise ValueError(f"Expression '{self}' have more than one value.")
|
|
302
|
+
return self.values[0]
|
|
303
|
+
|
|
304
|
+
@cached_property
|
|
305
|
+
def dtype(self):
|
|
306
|
+
return ott.get_type_by_objects(self.values)
|
|
307
|
+
|
|
308
|
+
@property
|
|
309
|
+
def is_emulator(self) -> bool:
|
|
310
|
+
try:
|
|
311
|
+
return isinstance(self.value, _EmulateObject)
|
|
312
|
+
except ValueError:
|
|
313
|
+
return False
|
|
314
|
+
|
|
315
|
+
@property
|
|
316
|
+
def is_state_vars(self) -> bool:
|
|
317
|
+
try:
|
|
318
|
+
return isinstance(self.value, _EmulateStateVars)
|
|
319
|
+
except ValueError:
|
|
320
|
+
return False
|
|
321
|
+
|
|
322
|
+
@property
|
|
323
|
+
def is_static(self) -> bool:
|
|
324
|
+
try:
|
|
325
|
+
return isinstance(self.value, Static)
|
|
326
|
+
except ValueError:
|
|
327
|
+
return False
|
|
328
|
+
|
|
329
|
+
@property
|
|
330
|
+
def is_dynamic_tick(self) -> bool:
|
|
331
|
+
try:
|
|
332
|
+
return isinstance(self.value, _DynamicTick)
|
|
333
|
+
except ValueError:
|
|
334
|
+
return False
|
|
335
|
+
|
|
336
|
+
@property
|
|
337
|
+
def is_tick(self) -> bool:
|
|
338
|
+
try:
|
|
339
|
+
return isinstance(self.value, _TickSequenceTickBase)
|
|
340
|
+
except ValueError:
|
|
341
|
+
return False
|
|
342
|
+
|
|
343
|
+
@property
|
|
344
|
+
def is_column(self) -> bool:
|
|
345
|
+
try:
|
|
346
|
+
return isinstance(self.value, _Column)
|
|
347
|
+
except ValueError:
|
|
348
|
+
return False
|
|
349
|
+
|
|
350
|
+
@property
|
|
351
|
+
def is_local_variable(self) -> bool:
|
|
352
|
+
try:
|
|
353
|
+
return isinstance(self.value, LocalVariable)
|
|
354
|
+
except ValueError:
|
|
355
|
+
return False
|
|
356
|
+
|
|
357
|
+
@property
|
|
358
|
+
def is_operation(self) -> bool:
|
|
359
|
+
try:
|
|
360
|
+
return isinstance(self.value, _Operation)
|
|
361
|
+
except ValueError:
|
|
362
|
+
return False
|
|
363
|
+
|
|
364
|
+
@property
|
|
365
|
+
def predefined(self) -> bool:
|
|
366
|
+
"""Check if the value of expression is known before the execution of query"""
|
|
367
|
+
return not self.is_operation
|
|
368
|
+
|
|
369
|
+
@property
|
|
370
|
+
def expressible(self) -> bool:
|
|
371
|
+
return bool(self.expr)
|
|
372
|
+
|
|
373
|
+
def __str__(self):
|
|
374
|
+
if not self.expressible:
|
|
375
|
+
raise ValueError("This Expression can't be expressed in OneTick or is undefined yet")
|
|
376
|
+
return self.expr
|
|
377
|
+
|
|
378
|
+
def convert_to_operation(self):
|
|
379
|
+
"""
|
|
380
|
+
Convert otp.Column to otp.Operation.
|
|
381
|
+
Needed to convert expressions like:
|
|
382
|
+
if tick['X']:
|
|
383
|
+
to
|
|
384
|
+
if (X != 0) {
|
|
385
|
+
"""
|
|
386
|
+
if self.is_column:
|
|
387
|
+
self.values = [self.value._make_python_way_bool_expression()]
|
|
388
|
+
self._expr = str(self.value)
|
|
389
|
+
|
|
390
|
+
@staticmethod
|
|
391
|
+
def value_to_onetick(value: Union[str, int, float, bool, None, _Operation]) -> str:
|
|
392
|
+
"""
|
|
393
|
+
Python value will be converted accordingly to OneTick syntax
|
|
394
|
+
(lowercase boolean values, string in quotes, etc.)
|
|
395
|
+
"""
|
|
396
|
+
if value is None:
|
|
397
|
+
return str(ott.nan)
|
|
398
|
+
if isinstance(value, bool):
|
|
399
|
+
return str(value).lower()
|
|
400
|
+
return ott.value2str(value)
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
class CaseOperatorParser:
|
|
404
|
+
"""
|
|
405
|
+
Class with methods to convert ast operators to their string or python representations.
|
|
406
|
+
Only ast operators that can be used in OneTick's CASE function are accepted.
|
|
407
|
+
"""
|
|
408
|
+
|
|
409
|
+
@staticmethod
|
|
410
|
+
def py_operator(op: Union[ast.operator, ast.cmpop, ast.unaryop, ast.boolop]) -> Callable:
|
|
411
|
+
"""
|
|
412
|
+
Convert ast operator to python function for this operator.
|
|
413
|
+
|
|
414
|
+
Parameters
|
|
415
|
+
----------
|
|
416
|
+
op
|
|
417
|
+
ast operator object
|
|
418
|
+
"""
|
|
419
|
+
return {
|
|
420
|
+
# binary
|
|
421
|
+
ast.Add: operator.add,
|
|
422
|
+
ast.Sub: operator.sub,
|
|
423
|
+
ast.Mult: operator.mul,
|
|
424
|
+
ast.Div: operator.truediv,
|
|
425
|
+
ast.BitAnd: operator.and_,
|
|
426
|
+
ast.BitOr: operator.or_,
|
|
427
|
+
ast.Mod: operator.mod,
|
|
428
|
+
# unary
|
|
429
|
+
ast.UAdd: operator.pos,
|
|
430
|
+
ast.USub: operator.neg,
|
|
431
|
+
ast.Not: operator.not_,
|
|
432
|
+
ast.Invert: operator.invert,
|
|
433
|
+
# compare
|
|
434
|
+
ast.Lt: operator.lt,
|
|
435
|
+
ast.LtE: operator.le,
|
|
436
|
+
ast.Gt: operator.gt,
|
|
437
|
+
ast.GtE: operator.ge,
|
|
438
|
+
ast.Eq: operator.eq,
|
|
439
|
+
ast.NotEq: operator.ne,
|
|
440
|
+
# bool
|
|
441
|
+
ast.And: lambda x, y: x and y,
|
|
442
|
+
ast.Or: lambda x, y: x or y,
|
|
443
|
+
}[type(op)] # type: ignore[return-value]
|
|
444
|
+
|
|
445
|
+
@staticmethod
|
|
446
|
+
def operator(op: Union[ast.operator, ast.cmpop, ast.unaryop, ast.boolop]) -> str:
|
|
447
|
+
"""
|
|
448
|
+
Convert ast operator to OneTick's string representation.
|
|
449
|
+
|
|
450
|
+
Parameters
|
|
451
|
+
----------
|
|
452
|
+
op
|
|
453
|
+
ast operator object
|
|
454
|
+
"""
|
|
455
|
+
return {
|
|
456
|
+
# binary
|
|
457
|
+
ast.Add: '+',
|
|
458
|
+
ast.Sub: '-',
|
|
459
|
+
ast.Mult: '*',
|
|
460
|
+
ast.Div: '/',
|
|
461
|
+
ast.Mod: '%',
|
|
462
|
+
# unary
|
|
463
|
+
ast.UAdd: '+',
|
|
464
|
+
ast.USub: '-',
|
|
465
|
+
# compare
|
|
466
|
+
ast.Lt: '<',
|
|
467
|
+
ast.LtE: '<=',
|
|
468
|
+
ast.Gt: '>',
|
|
469
|
+
ast.GtE: '>=',
|
|
470
|
+
ast.Eq: '=',
|
|
471
|
+
ast.NotEq: '!=',
|
|
472
|
+
# bool
|
|
473
|
+
ast.And: 'AND',
|
|
474
|
+
ast.Or: 'OR',
|
|
475
|
+
}[type(op)]
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
class OperatorParser(CaseOperatorParser):
|
|
479
|
+
"""
|
|
480
|
+
Class with methods to convert ast operators to their string or python representations.
|
|
481
|
+
Only ast operators that can be used in OneTick's per tick script are accepted.
|
|
482
|
+
"""
|
|
483
|
+
|
|
484
|
+
@staticmethod
|
|
485
|
+
def py_operator(op: Union[ast.operator, ast.cmpop, ast.unaryop, ast.boolop],
|
|
486
|
+
aug: bool = False, **kwargs) -> Callable:
|
|
487
|
+
"""
|
|
488
|
+
Convert ast operator to python function for this operator.
|
|
489
|
+
|
|
490
|
+
Parameters
|
|
491
|
+
----------
|
|
492
|
+
op
|
|
493
|
+
ast operator object
|
|
494
|
+
aug
|
|
495
|
+
ast don't have separate inplace operators (+=, -=, etc.)
|
|
496
|
+
If this parameter is True then operator is inplace and otherwise if False.
|
|
497
|
+
"""
|
|
498
|
+
if aug:
|
|
499
|
+
assert isinstance(op, ast.operator)
|
|
500
|
+
return {
|
|
501
|
+
ast.Add: operator.iadd,
|
|
502
|
+
ast.Sub: operator.isub,
|
|
503
|
+
ast.Mult: operator.imul,
|
|
504
|
+
ast.Div: operator.itruediv,
|
|
505
|
+
}[type(op)]
|
|
506
|
+
return CaseOperatorParser.py_operator(op, **kwargs)
|
|
507
|
+
|
|
508
|
+
@staticmethod
|
|
509
|
+
def operator(op: Union[ast.operator, ast.cmpop, ast.unaryop, ast.boolop],
|
|
510
|
+
aug: bool = False) -> str:
|
|
511
|
+
"""
|
|
512
|
+
Convert ast operator to its string representation.
|
|
513
|
+
|
|
514
|
+
Parameters
|
|
515
|
+
----------
|
|
516
|
+
op
|
|
517
|
+
ast operator object
|
|
518
|
+
aug
|
|
519
|
+
ast don't have separate inplace binary operators (+=, -=, etc.)
|
|
520
|
+
If this parameter is True then parameter is inplace and otherwise if False.
|
|
521
|
+
"""
|
|
522
|
+
if aug:
|
|
523
|
+
assert isinstance(op, ast.operator)
|
|
524
|
+
return {
|
|
525
|
+
ast.Add: '+=',
|
|
526
|
+
ast.Sub: '-=',
|
|
527
|
+
ast.Mult: '*=',
|
|
528
|
+
ast.Div: '/=',
|
|
529
|
+
}[type(op)]
|
|
530
|
+
try:
|
|
531
|
+
return {
|
|
532
|
+
ast.Eq: '==',
|
|
533
|
+
ast.And: '&&',
|
|
534
|
+
ast.Or: '||',
|
|
535
|
+
}[type(op)]
|
|
536
|
+
except KeyError:
|
|
537
|
+
return CaseOperatorParser.operator(op)
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
class ExpressionParser:
|
|
541
|
+
"""
|
|
542
|
+
Class with methods to convert ast expressions to OneTick's script or function syntax.
|
|
543
|
+
"""
|
|
544
|
+
def __init__(self, fun: 'FunctionParser'):
|
|
545
|
+
self.fun = fun
|
|
546
|
+
self.operator_parser = OperatorParser()
|
|
547
|
+
|
|
548
|
+
@contextmanager
|
|
549
|
+
def _replace_context(self, closure_vars: inspect.ClosureVars):
|
|
550
|
+
"""
|
|
551
|
+
Temporarily change closure variables in self.fun.
|
|
552
|
+
Variables will be replaced with those from closure_vars parameter.
|
|
553
|
+
"""
|
|
554
|
+
nonlocals, globals_, *_ = closure_vars
|
|
555
|
+
assert isinstance(self.fun.closure_vars.globals, dict)
|
|
556
|
+
assert isinstance(self.fun.closure_vars.nonlocals, dict)
|
|
557
|
+
saved_globals = self.fun.closure_vars.globals.copy()
|
|
558
|
+
saved_nonlocals = self.fun.closure_vars.nonlocals.copy()
|
|
559
|
+
self.fun.closure_vars.globals.update(globals_)
|
|
560
|
+
self.fun.closure_vars.nonlocals.update(nonlocals)
|
|
561
|
+
yield
|
|
562
|
+
self.fun.closure_vars.globals.update(saved_globals)
|
|
563
|
+
self.fun.closure_vars.nonlocals.update(saved_nonlocals)
|
|
564
|
+
|
|
565
|
+
def constant(self, expr: ast.Constant) -> Expression:
|
|
566
|
+
"""Some basic constant value: string, integer, float."""
|
|
567
|
+
return Expression(expr.value)
|
|
568
|
+
|
|
569
|
+
def string(self, expr: "ast.Str") -> Expression:
|
|
570
|
+
"""String (for backward compatibility with Python 3.7)."""
|
|
571
|
+
return Expression(expr.s)
|
|
572
|
+
|
|
573
|
+
def number(self, expr: "ast.Str") -> Expression:
|
|
574
|
+
"""Number (for backward compatibility with Python 3.7)."""
|
|
575
|
+
return Expression(expr.n)
|
|
576
|
+
|
|
577
|
+
def name(self, expr: ast.Name) -> Expression:
|
|
578
|
+
"""
|
|
579
|
+
Name of the variable.
|
|
580
|
+
Every variable in per-tick script function, if defined correctly,
|
|
581
|
+
is considered to be local per-tick script variable.
|
|
582
|
+
If variable with this name is not found it will be captured from function context.
|
|
583
|
+
"""
|
|
584
|
+
if self.fun.arg_name and expr.id == self.fun.arg_name:
|
|
585
|
+
value = self.fun.emulator if self.fun.emulator is not None else expr.id
|
|
586
|
+
return Expression(value)
|
|
587
|
+
|
|
588
|
+
if not isinstance(expr.ctx, ast.Load):
|
|
589
|
+
# local variable, left-hand side
|
|
590
|
+
return Expression(LocalVariable(expr.id))
|
|
591
|
+
|
|
592
|
+
for dict_name in ('LOCAL_VARS', 'STATIC_VARS'):
|
|
593
|
+
# local or static variable, right-hand side
|
|
594
|
+
variables = getattr(self.fun.emulator, dict_name, {})
|
|
595
|
+
if expr.id in variables:
|
|
596
|
+
dtype = ott.get_type_by_objects([variables[expr.id]])
|
|
597
|
+
if issubclass(dtype, _TickSequenceTickBase):
|
|
598
|
+
# ticks have schema, owner and methods, so using saved value
|
|
599
|
+
return Expression(variables[expr.id])
|
|
600
|
+
return Expression(LocalVariable(expr.id, dtype))
|
|
601
|
+
|
|
602
|
+
if not self.fun._from_args_annotations and expr.id in self.fun.args_annotations:
|
|
603
|
+
# parameter of the per-tick script function
|
|
604
|
+
return Expression(_Operation(op_str=expr.id, dtype=self.fun.args_annotations[expr.id]))
|
|
605
|
+
|
|
606
|
+
# pylint: disable-next=eval-used
|
|
607
|
+
value = eval(expr.id, self.fun.closure_vars.globals, self.fun.closure_vars.nonlocals) # type: ignore[arg-type]
|
|
608
|
+
return Expression(value)
|
|
609
|
+
|
|
610
|
+
def index(self, expr: ast.Index) -> Expression:
|
|
611
|
+
"""Proxy object in ast.Subscript in python < 3.9"""
|
|
612
|
+
return self.expression(expr.value) # type: ignore[attr-defined]
|
|
613
|
+
|
|
614
|
+
def slice(self, expr: ast.Slice) -> Expression:
|
|
615
|
+
"""
|
|
616
|
+
Slice of the list.
|
|
617
|
+
For example:
|
|
618
|
+
a = [1, 2, 3, 4]
|
|
619
|
+
a[2:4]
|
|
620
|
+
Here, 2:4 is the slice.
|
|
621
|
+
"""
|
|
622
|
+
lower = self.expression(expr.lower).value if expr.lower else None
|
|
623
|
+
upper = self.expression(expr.upper).value if expr.upper else None
|
|
624
|
+
step = self.expression(expr.step).value if expr.step else None
|
|
625
|
+
return Expression(slice(lower, upper, step))
|
|
626
|
+
|
|
627
|
+
def subscript(self, expr: ast.Subscript) -> Expression:
|
|
628
|
+
"""
|
|
629
|
+
Expression like: tick['X'].
|
|
630
|
+
Setting items of ticks and state variables is supported.
|
|
631
|
+
Getting items supported for any captured variable.
|
|
632
|
+
"""
|
|
633
|
+
val = self.expression(expr.value)
|
|
634
|
+
item = self.expression(expr.slice)
|
|
635
|
+
|
|
636
|
+
if isinstance(expr.ctx, ast.Load):
|
|
637
|
+
v = val.value[item.value]
|
|
638
|
+
return Expression(v)
|
|
639
|
+
|
|
640
|
+
# index of per tick script function parameter or tick sequence tick is column name
|
|
641
|
+
if not (val.is_emulator or val.is_tick or val.is_state_vars):
|
|
642
|
+
raise ValueError(f"Setting items supported only for "
|
|
643
|
+
f"'{self.fun.arg_name}' function argument, "
|
|
644
|
+
f"tick sequences' ticks and state variables object")
|
|
645
|
+
|
|
646
|
+
return Expression(
|
|
647
|
+
lambda rhs: val.value.__setitem__(item.value, rhs),
|
|
648
|
+
expr=item.value,
|
|
649
|
+
lhs=True,
|
|
650
|
+
)
|
|
651
|
+
|
|
652
|
+
def attribute(self, expr: ast.Attribute) -> Expression:
|
|
653
|
+
"""
|
|
654
|
+
Expression like: tick.X
|
|
655
|
+
For now we only support setting attributes of first function parameter.
|
|
656
|
+
Getting attributes supported for any captured variable.
|
|
657
|
+
"""
|
|
658
|
+
val = self.expression(expr.value)
|
|
659
|
+
attr = expr.attr
|
|
660
|
+
|
|
661
|
+
if isinstance(expr.ctx, ast.Load):
|
|
662
|
+
v = getattr(val.value, attr)
|
|
663
|
+
return Expression(v)
|
|
664
|
+
|
|
665
|
+
# attribute of per tick script function parameter or tick sequence tick is column name
|
|
666
|
+
if not (val.is_emulator or val.is_tick):
|
|
667
|
+
raise ValueError(f"Setting attributes supported only for "
|
|
668
|
+
f"'{self.fun.arg_name}' function argument and tick sequences' ticks")
|
|
669
|
+
|
|
670
|
+
return Expression(
|
|
671
|
+
lambda rhs: val.value.__setattr__(attr, rhs),
|
|
672
|
+
expr=attr,
|
|
673
|
+
lhs=True,
|
|
674
|
+
)
|
|
675
|
+
|
|
676
|
+
def bin_op(self, expr: ast.BinOp) -> Expression:
|
|
677
|
+
"""
|
|
678
|
+
Binary operation expression: 2 + 2, tick['X'] * 2, etc.
|
|
679
|
+
"""
|
|
680
|
+
left = self.expression(expr.left)
|
|
681
|
+
py_op = self.operator_parser.py_operator(expr.op)
|
|
682
|
+
right = self.expression(expr.right)
|
|
683
|
+
value = py_op(left.value, right.value)
|
|
684
|
+
return Expression(value)
|
|
685
|
+
|
|
686
|
+
def unary_op(self, expr: ast.UnaryOp) -> Expression:
|
|
687
|
+
"""
|
|
688
|
+
Unary operation expression: -1, -tick['X'], not tick['X'], ~tick['X'], etc.
|
|
689
|
+
"""
|
|
690
|
+
py_op = self.operator_parser.py_operator(expr.op)
|
|
691
|
+
operand = self.expression(expr.operand)
|
|
692
|
+
if operand.is_operation:
|
|
693
|
+
# special case for negative otp.Columns and otp.Operations
|
|
694
|
+
if isinstance(expr.op, (ast.Not, ast.Invert)):
|
|
695
|
+
operand.convert_to_operation()
|
|
696
|
+
if isinstance(expr.op, ast.Not):
|
|
697
|
+
py_op = self.operator_parser.py_operator(ast.Invert())
|
|
698
|
+
value = py_op(operand.value)
|
|
699
|
+
return Expression(value)
|
|
700
|
+
|
|
701
|
+
def bool_op(self, expr: ast.BoolOp) -> Expression:
|
|
702
|
+
"""
|
|
703
|
+
Bool operation expression: True and tick['X'], etc.
|
|
704
|
+
Note that
|
|
705
|
+
* all python values will be checked inplace and will not be written to the script
|
|
706
|
+
* short-circuit logic will work for python values
|
|
707
|
+
For example:
|
|
708
|
+
True and 0 and tick['X'] == 1 -------> false
|
|
709
|
+
'true' or False or tick['X'] == 1 -------> true
|
|
710
|
+
True and True and tick['X'] == 1 -------> X == 1
|
|
711
|
+
"""
|
|
712
|
+
value = None
|
|
713
|
+
for e in expr.values:
|
|
714
|
+
expression = self.expression(e)
|
|
715
|
+
expression.convert_to_operation()
|
|
716
|
+
v = expression.value
|
|
717
|
+
|
|
718
|
+
if not expression.is_operation:
|
|
719
|
+
# short-circuit logic, return as early as possible
|
|
720
|
+
if isinstance(expr.op, ast.And) and not v:
|
|
721
|
+
# TODO: return v, not True or False
|
|
722
|
+
# TODO: there can be many values if operations are present
|
|
723
|
+
value = False
|
|
724
|
+
break
|
|
725
|
+
if isinstance(expr.op, ast.Or) and v:
|
|
726
|
+
value = True
|
|
727
|
+
break
|
|
728
|
+
continue
|
|
729
|
+
|
|
730
|
+
if value is None:
|
|
731
|
+
value = v
|
|
732
|
+
continue
|
|
733
|
+
|
|
734
|
+
if isinstance(value, _Operation) or expression.is_operation:
|
|
735
|
+
# change operator for operations
|
|
736
|
+
py_op = self.operator_parser.py_operator({
|
|
737
|
+
ast.And: ast.BitAnd(),
|
|
738
|
+
ast.Or: ast.BitOr(),
|
|
739
|
+
}[type(expr.op)])
|
|
740
|
+
else:
|
|
741
|
+
py_op = self.operator_parser.py_operator(expr.op)
|
|
742
|
+
|
|
743
|
+
value = py_op(value, v)
|
|
744
|
+
return Expression(value)
|
|
745
|
+
|
|
746
|
+
def _convert_in_to_bool_op(self, expr: ast.Compare) -> Union[ast.Compare, ast.BoolOp]:
|
|
747
|
+
"""
|
|
748
|
+
Convert expressions like:
|
|
749
|
+
tick['X'] in [1, 2] -----> tick['X'] == 1 or tick['X'] == 2
|
|
750
|
+
tick['X'] not in [1, 2] -----> tick['X'] != 1 and tick['X'] != 2
|
|
751
|
+
"""
|
|
752
|
+
left, op, right = expr.left, expr.ops[0], expr.comparators[0]
|
|
753
|
+
if not isinstance(op, (ast.In, ast.NotIn)):
|
|
754
|
+
return expr
|
|
755
|
+
|
|
756
|
+
assert len(expr.ops) == 1
|
|
757
|
+
assert len(expr.comparators) == 1
|
|
758
|
+
|
|
759
|
+
right_value = self.expression(right).value
|
|
760
|
+
if isinstance(right_value, range) and right_value.step == 1:
|
|
761
|
+
# replace tick['X'] in range(5, 10) to X >=5 AND X < 10
|
|
762
|
+
return ast.BoolOp(
|
|
763
|
+
op=ast.And(),
|
|
764
|
+
values=[
|
|
765
|
+
ast.Compare(left=left, ops=[ast.GtE()], comparators=[ast.Constant(right_value.start)]),
|
|
766
|
+
ast.Compare(left=left, ops=[ast.Lt()], comparators=[ast.Constant(right_value.stop)]),
|
|
767
|
+
]
|
|
768
|
+
)
|
|
769
|
+
|
|
770
|
+
right_values = [ast.Constant(r) for r in right_value]
|
|
771
|
+
|
|
772
|
+
bool_op: ast.boolop
|
|
773
|
+
compare_op: ast.cmpop
|
|
774
|
+
if isinstance(op, ast.In):
|
|
775
|
+
bool_op, compare_op = ast.Or(), ast.Eq()
|
|
776
|
+
else:
|
|
777
|
+
bool_op, compare_op = ast.And(), ast.NotEq()
|
|
778
|
+
|
|
779
|
+
values: List[ast.expr] = [
|
|
780
|
+
ast.Compare(left=left, ops=[compare_op], comparators=[r])
|
|
781
|
+
for r in right_values
|
|
782
|
+
]
|
|
783
|
+
return ast.BoolOp(op=bool_op, values=values)
|
|
784
|
+
|
|
785
|
+
def _convert_many_comparators_to_bool_op(self, expr: ast.Compare) -> Union[ast.Compare, ast.BoolOp]:
|
|
786
|
+
"""
|
|
787
|
+
OneTick don't support compare expressions with many comparators
|
|
788
|
+
so replacing them with several simple expressions.
|
|
789
|
+
For example:
|
|
790
|
+
1 < tick['X'] < 3, -----> tick['X'] > 1 AND tick['X'] < 3
|
|
791
|
+
"""
|
|
792
|
+
if len(expr.comparators) == 1 and len(expr.ops) == 1:
|
|
793
|
+
return expr
|
|
794
|
+
|
|
795
|
+
comparators = []
|
|
796
|
+
comparators.append(expr.left)
|
|
797
|
+
ops = []
|
|
798
|
+
for op, right in zip(expr.ops, expr.comparators):
|
|
799
|
+
ops.append(op)
|
|
800
|
+
comparators.append(right)
|
|
801
|
+
|
|
802
|
+
bool_operands: List[ast.expr] = []
|
|
803
|
+
for i in range(len(comparators) - 1):
|
|
804
|
+
left, op, right = comparators[i], ops[i], comparators[i + 1]
|
|
805
|
+
bool_operands.append(
|
|
806
|
+
ast.Compare(left=left, ops=[op], comparators=[right])
|
|
807
|
+
)
|
|
808
|
+
return ast.BoolOp(op=ast.And(), values=bool_operands)
|
|
809
|
+
|
|
810
|
+
def compare(self, expr: ast.Compare) -> Expression:
|
|
811
|
+
"""
|
|
812
|
+
Compare operation expression: tick['X'] > 1, 1 < 2 < 3, tick['X'] in [1, 2] etc.
|
|
813
|
+
"""
|
|
814
|
+
if len(expr.ops) > 1:
|
|
815
|
+
return self.expression(
|
|
816
|
+
self._convert_many_comparators_to_bool_op(expr)
|
|
817
|
+
)
|
|
818
|
+
|
|
819
|
+
op = expr.ops[0]
|
|
820
|
+
if isinstance(op, (ast.In, ast.NotIn)):
|
|
821
|
+
return self.expression(
|
|
822
|
+
self._convert_in_to_bool_op(expr)
|
|
823
|
+
)
|
|
824
|
+
|
|
825
|
+
left = self.expression(expr.left)
|
|
826
|
+
right = self.expression(expr.comparators[0])
|
|
827
|
+
|
|
828
|
+
py_op = self.operator_parser.py_operator(op)
|
|
829
|
+
value = py_op(left.value, right.value)
|
|
830
|
+
return Expression(value)
|
|
831
|
+
|
|
832
|
+
def keyword(self, expr: ast.keyword) -> Tuple[str, Any]:
|
|
833
|
+
"""
|
|
834
|
+
Keyword argument expression from function call: func(key=value).
|
|
835
|
+
Not converted to per tick script in any way, needed only in self.call() function.
|
|
836
|
+
"""
|
|
837
|
+
arg = expr.arg
|
|
838
|
+
val = self.expression(expr.value)
|
|
839
|
+
assert arg is not None
|
|
840
|
+
return arg, val.value
|
|
841
|
+
|
|
842
|
+
def call(self, expr: ast.Call) -> Expression:
|
|
843
|
+
"""
|
|
844
|
+
Any call expression, like otp.nsectime().
|
|
845
|
+
The returned value of the call will be inserted in script.
|
|
846
|
+
"""
|
|
847
|
+
func = self.expression(expr.func)
|
|
848
|
+
|
|
849
|
+
# TODO: refactor, this code is copy-pasted from CaseExpressionParser.call()
|
|
850
|
+
need_to_parse = False
|
|
851
|
+
if not isinstance(func.value, types.BuiltinMethodType) and expr.args:
|
|
852
|
+
node = expr.args[0]
|
|
853
|
+
if isinstance(node, ast.Name) and node.id == self.fun.arg_name:
|
|
854
|
+
# we will parse inner function call to OneTick per-tick script function
|
|
855
|
+
# only if one of the function call arguments is 'tick' parameter of the original function
|
|
856
|
+
need_to_parse = True
|
|
857
|
+
|
|
858
|
+
if need_to_parse:
|
|
859
|
+
err = None
|
|
860
|
+
func_name = func.value.__name__
|
|
861
|
+
if str(func.value) not in self.fun.emulator.FUNCTIONS:
|
|
862
|
+
try:
|
|
863
|
+
fp = FunctionParser(func.value, emulator=self.fun.emulator,
|
|
864
|
+
check_arg_name=self.fun.arg_name, inner_function=True)
|
|
865
|
+
except Exception as e:
|
|
866
|
+
err = e
|
|
867
|
+
else:
|
|
868
|
+
value = fp.per_tick_script()
|
|
869
|
+
self.fun.emulator.FUNCTIONS[str(func.value)] = (value, fp.args_annotations, fp.return_annotation)
|
|
870
|
+
if err is None:
|
|
871
|
+
_, args_annotations, return_type = self.fun.emulator.FUNCTIONS[str(func.value)]
|
|
872
|
+
if expr.keywords:
|
|
873
|
+
raise ValueError(
|
|
874
|
+
f"Passing keyword parameters to per-tick script function '{func_name}' is not supported"
|
|
875
|
+
)
|
|
876
|
+
args = []
|
|
877
|
+
for arg, (name, dtype) in zip(expr.args[1:], args_annotations.items()):
|
|
878
|
+
if isinstance(arg, ast.Starred):
|
|
879
|
+
raise ValueError(
|
|
880
|
+
f"Passing starred parameter to per-tick script function '{func_name}' is not supported"
|
|
881
|
+
)
|
|
882
|
+
arg_expr = self.expression(arg)
|
|
883
|
+
msg = (f"In function '{func_name}' parameter '{name}'"
|
|
884
|
+
f" has type annotation '{dtype.__name__}',"
|
|
885
|
+
f" but the type of passed argument is '{arg_expr.dtype.__name__}'")
|
|
886
|
+
try:
|
|
887
|
+
widest_type = ott.get_type_by_objects([arg_expr.dtype, dtype])
|
|
888
|
+
except TypeError as e:
|
|
889
|
+
raise TypeError(msg) from e
|
|
890
|
+
if widest_type is not arg_expr.dtype:
|
|
891
|
+
raise TypeError(msg)
|
|
892
|
+
args.append(arg_expr)
|
|
893
|
+
str_args = ', '.join(map(str, args))
|
|
894
|
+
return Expression(_Operation(op_str=f'{func_name}({str_args})', dtype=return_type))
|
|
895
|
+
|
|
896
|
+
args = []
|
|
897
|
+
for arg in expr.args:
|
|
898
|
+
# TODO: support starred in CaseExpressionParser.call()
|
|
899
|
+
if isinstance(arg, ast.Starred):
|
|
900
|
+
args.extend(self.expression(arg.value).value)
|
|
901
|
+
else:
|
|
902
|
+
args.append(self.expression(arg).value)
|
|
903
|
+
keywords = dict(self.keyword(keyword) for keyword in expr.keywords)
|
|
904
|
+
value = func.value(*args, **keywords)
|
|
905
|
+
return Expression(value)
|
|
906
|
+
|
|
907
|
+
def formatted_value(self, expr: ast.FormattedValue) -> Expression:
|
|
908
|
+
"""
|
|
909
|
+
Block from the f-string in curly brackets, e.g.
|
|
910
|
+
{tick['A']} and {123} in f"{tick['A']} {123}"
|
|
911
|
+
"""
|
|
912
|
+
return self.expression(expr.value)
|
|
913
|
+
|
|
914
|
+
def joined_str(self, expr: ast.JoinedStr) -> Expression:
|
|
915
|
+
"""
|
|
916
|
+
F-string expression, like: f"{tick['A']} {123}"
|
|
917
|
+
"""
|
|
918
|
+
expressions = [self.expression(value) for value in expr.values]
|
|
919
|
+
value = None
|
|
920
|
+
for expression in expressions:
|
|
921
|
+
v = expression.value
|
|
922
|
+
if expression.is_operation:
|
|
923
|
+
v = v.apply(str)
|
|
924
|
+
else:
|
|
925
|
+
v = str(v)
|
|
926
|
+
if value is None:
|
|
927
|
+
value = v
|
|
928
|
+
else:
|
|
929
|
+
value = value + v
|
|
930
|
+
return Expression(value)
|
|
931
|
+
|
|
932
|
+
def list(self, expr: ast.List) -> Expression:
|
|
933
|
+
"""
|
|
934
|
+
List expression, like: [1, 2, 3, 4, 5]
|
|
935
|
+
"""
|
|
936
|
+
value = []
|
|
937
|
+
for e in expr.elts:
|
|
938
|
+
if isinstance(e, ast.Starred):
|
|
939
|
+
value.extend(self.expression(e.value).value)
|
|
940
|
+
else:
|
|
941
|
+
value.append(self.expression(e).value)
|
|
942
|
+
return Expression(value, expr=None)
|
|
943
|
+
|
|
944
|
+
def tuple(self, expr: ast.Tuple) -> Expression:
|
|
945
|
+
"""
|
|
946
|
+
Tuple expression, like: (1, 2, 3, 4, 5)
|
|
947
|
+
"""
|
|
948
|
+
expression = self.list(expr) # type: ignore[arg-type]
|
|
949
|
+
expression.values = (tuple(expression.value),)
|
|
950
|
+
return expression
|
|
951
|
+
|
|
952
|
+
@property
|
|
953
|
+
def _expression(self) -> dict:
|
|
954
|
+
"""Mapping from ast expression to parser functions"""
|
|
955
|
+
mapping = {
|
|
956
|
+
ast.Constant: self.constant,
|
|
957
|
+
ast.Name: self.name,
|
|
958
|
+
ast.Attribute: self.attribute,
|
|
959
|
+
ast.Index: self.index,
|
|
960
|
+
ast.Subscript: self.subscript,
|
|
961
|
+
ast.BinOp: self.bin_op,
|
|
962
|
+
ast.UnaryOp: self.unary_op,
|
|
963
|
+
ast.BoolOp: self.bool_op,
|
|
964
|
+
ast.Compare: self.compare,
|
|
965
|
+
ast.Call: self.call,
|
|
966
|
+
ast.FormattedValue: self.formatted_value,
|
|
967
|
+
ast.JoinedStr: self.joined_str,
|
|
968
|
+
ast.List: self.list,
|
|
969
|
+
ast.Tuple: self.tuple,
|
|
970
|
+
ast.Slice: self.slice,
|
|
971
|
+
}
|
|
972
|
+
deprecated = {
|
|
973
|
+
'NameConstant': self.constant,
|
|
974
|
+
'Str': self.string,
|
|
975
|
+
'Num': self.number,
|
|
976
|
+
}
|
|
977
|
+
with warnings.catch_warnings():
|
|
978
|
+
warnings.simplefilter('ignore', DeprecationWarning)
|
|
979
|
+
for name, callback in deprecated.items():
|
|
980
|
+
if hasattr(ast, name):
|
|
981
|
+
mapping[getattr(ast, name)] = callback
|
|
982
|
+
return mapping
|
|
983
|
+
|
|
984
|
+
def expression(self, expr: ast.expr) -> Expression:
|
|
985
|
+
"""Return parsed expression according to its type."""
|
|
986
|
+
return self._expression[type(expr)](expr)
|
|
987
|
+
|
|
988
|
+
|
|
989
|
+
class CaseExpressionParser(ExpressionParser):
|
|
990
|
+
"""
|
|
991
|
+
Class with methods to convert ast expressions to CASE function.
|
|
992
|
+
"""
|
|
993
|
+
def __init__(self, fun: 'FunctionParser'):
|
|
994
|
+
super().__init__(fun)
|
|
995
|
+
self.operator_parser = CaseOperatorParser() # type: ignore[assignment]
|
|
996
|
+
|
|
997
|
+
def _convert_bool_op_to_if_expr(self, expr: ast.expr) -> ast.expr:
|
|
998
|
+
"""
|
|
999
|
+
Special case to convert bool operation to if expression.
|
|
1000
|
+
For example:
|
|
1001
|
+
lambda row: row['A'] or -1
|
|
1002
|
+
will be converted to:
|
|
1003
|
+
CASE(A != 0, 1, A, -1)
|
|
1004
|
+
"""
|
|
1005
|
+
if not isinstance(expr, ast.BoolOp):
|
|
1006
|
+
return expr
|
|
1007
|
+
|
|
1008
|
+
def get_if_expr(first, second):
|
|
1009
|
+
if isinstance(expr.op, ast.Or):
|
|
1010
|
+
return ast.IfExp(test=first, body=first, orelse=second)
|
|
1011
|
+
else:
|
|
1012
|
+
return ast.IfExp(test=first, body=second, orelse=first)
|
|
1013
|
+
|
|
1014
|
+
first = None
|
|
1015
|
+
for i in range(len(expr.values) - 1):
|
|
1016
|
+
if first is None:
|
|
1017
|
+
first = expr.values[i]
|
|
1018
|
+
first = self._convert_bool_op_to_if_expr(first)
|
|
1019
|
+
second = expr.values[i + 1]
|
|
1020
|
+
second = self._convert_bool_op_to_if_expr(second)
|
|
1021
|
+
first = get_if_expr(first, second)
|
|
1022
|
+
assert first is not None
|
|
1023
|
+
return first
|
|
1024
|
+
|
|
1025
|
+
def if_expr(self, expr: ast.IfExp) -> Expression:
|
|
1026
|
+
"""
|
|
1027
|
+
If expression: 'A' if tick['X'] > 0 else 'B'.
|
|
1028
|
+
Do not confuse with if statement.
|
|
1029
|
+
Will be converted to OneTick case function: CASE(X > 0, 1, 'A', 'B').
|
|
1030
|
+
If condition value can be deduced before execution of script,
|
|
1031
|
+
then if or else value will be returned without using CASE() function.
|
|
1032
|
+
For example:
|
|
1033
|
+
tick['A'] if False else 3 -----------> 3
|
|
1034
|
+
"""
|
|
1035
|
+
test = self.expression(expr.test)
|
|
1036
|
+
if test.predefined:
|
|
1037
|
+
# we can remove unnecessary branch if condition value is already known
|
|
1038
|
+
if test.value:
|
|
1039
|
+
return self.expression(expr.body)
|
|
1040
|
+
return self.expression(expr.orelse)
|
|
1041
|
+
body = self.expression(expr.body)
|
|
1042
|
+
orelse = self.expression(expr.orelse)
|
|
1043
|
+
test.convert_to_operation()
|
|
1044
|
+
|
|
1045
|
+
str_expr = f'CASE({test}, 1, {body}, {orelse})'
|
|
1046
|
+
value = _LambdaIfElse(str_expr, ott.get_type_by_objects([*body.values, *orelse.values]))
|
|
1047
|
+
return Expression(value, expr=str_expr)
|
|
1048
|
+
|
|
1049
|
+
def call(self, expr: ast.Call) -> Expression:
|
|
1050
|
+
"""
|
|
1051
|
+
For CASE() function we support using inner functions that return valid case expression.
|
|
1052
|
+
"""
|
|
1053
|
+
func = self.expression(expr.func)
|
|
1054
|
+
|
|
1055
|
+
need_to_parse = False
|
|
1056
|
+
if not isinstance(func.value, types.BuiltinMethodType):
|
|
1057
|
+
for node in expr.args + [kw.value for kw in expr.keywords]:
|
|
1058
|
+
if isinstance(node, ast.Name) and node.id == self.fun.arg_name:
|
|
1059
|
+
# we will parse inner function call to OneTick expression
|
|
1060
|
+
# only if one of the function call arguments is
|
|
1061
|
+
# 'tick' or 'row' parameter of the original function
|
|
1062
|
+
need_to_parse = True
|
|
1063
|
+
break
|
|
1064
|
+
orig_err = None
|
|
1065
|
+
if not need_to_parse:
|
|
1066
|
+
with _CompareTrackScope(emulation_enabled=False):
|
|
1067
|
+
try:
|
|
1068
|
+
return super().call(expr)
|
|
1069
|
+
except Exception as err:
|
|
1070
|
+
orig_err = err
|
|
1071
|
+
uw = UserWarning(
|
|
1072
|
+
f"Function '{astunparse(expr)}' can't be called in python, "
|
|
1073
|
+
"will try to parse it to OneTick expression. "
|
|
1074
|
+
f"Use '{self.fun.arg_name}' in function's signature to indicate "
|
|
1075
|
+
"that this function can be parsed to OneTick expression."
|
|
1076
|
+
)
|
|
1077
|
+
uw.__cause__ = err
|
|
1078
|
+
warnings.warn(uw)
|
|
1079
|
+
|
|
1080
|
+
fp = FunctionParser(func.value, check_arg_name=False)
|
|
1081
|
+
|
|
1082
|
+
kwargs = {}
|
|
1083
|
+
args = fp.ast_node.args.args
|
|
1084
|
+
if fp.is_method:
|
|
1085
|
+
args = args[1:]
|
|
1086
|
+
for arg, default in zip(reversed(args), reversed(fp.ast_node.args.defaults)):
|
|
1087
|
+
kwargs[arg.arg] = default
|
|
1088
|
+
kwargs.update({keyword.arg: keyword.value for keyword in expr.keywords}) # type: ignore[misc]
|
|
1089
|
+
for arg, arg_value in zip(args, expr.args):
|
|
1090
|
+
kwargs[arg.arg] = arg_value
|
|
1091
|
+
|
|
1092
|
+
try:
|
|
1093
|
+
value = fp.compress()
|
|
1094
|
+
except Exception as err:
|
|
1095
|
+
try:
|
|
1096
|
+
return super().call(expr)
|
|
1097
|
+
except Exception:
|
|
1098
|
+
if orig_err is not None:
|
|
1099
|
+
raise err from orig_err
|
|
1100
|
+
raise ValueError(
|
|
1101
|
+
f"Can't convert function '{astunparse(expr)}' to CASE() expression."
|
|
1102
|
+
) from err
|
|
1103
|
+
try:
|
|
1104
|
+
with self._replace_context(fp.closure_vars):
|
|
1105
|
+
# replace function parameters with calculated values
|
|
1106
|
+
value = fp.case_statement_parser._replace_nodes(value, replace_name=kwargs)
|
|
1107
|
+
return self.expression(value)
|
|
1108
|
+
except Exception as err:
|
|
1109
|
+
if orig_err is not None:
|
|
1110
|
+
raise err from orig_err
|
|
1111
|
+
raise err
|
|
1112
|
+
|
|
1113
|
+
@property
|
|
1114
|
+
def _expression(self) -> dict:
|
|
1115
|
+
return dict(super()._expression.items() | {
|
|
1116
|
+
ast.IfExp: self.if_expr,
|
|
1117
|
+
}.items())
|
|
1118
|
+
|
|
1119
|
+
|
|
1120
|
+
class CaseStatementParser:
|
|
1121
|
+
"""
|
|
1122
|
+
Class with methods to convert ast statements to CASE function.
|
|
1123
|
+
"""
|
|
1124
|
+
def __init__(self, fun: 'FunctionParser'):
|
|
1125
|
+
self.fun = fun
|
|
1126
|
+
self.expression_parser = CaseExpressionParser(fun=fun)
|
|
1127
|
+
self.operator_parser = CaseOperatorParser()
|
|
1128
|
+
|
|
1129
|
+
T = TypeVar('T', bound=ast.AST)
|
|
1130
|
+
|
|
1131
|
+
@staticmethod
|
|
1132
|
+
def _replace_nodes(node: T,
|
|
1133
|
+
replace_name: Optional[Dict[str, ast.expr]] = None,
|
|
1134
|
+
replace_break: Union[ast.stmt, Exception, Type[Exception], None] = None,
|
|
1135
|
+
inplace: bool = False) -> T:
|
|
1136
|
+
"""
|
|
1137
|
+
Function to replace expressions and statements inside ast.For node.
|
|
1138
|
+
|
|
1139
|
+
Parameters
|
|
1140
|
+
----------
|
|
1141
|
+
node
|
|
1142
|
+
ast node in which expressions and statements will be replaced
|
|
1143
|
+
inplace
|
|
1144
|
+
if True `node` object will be modified else it will be copied and the copy will be returned
|
|
1145
|
+
replace_name
|
|
1146
|
+
mapping from ast.Name ids to ast expressions.
|
|
1147
|
+
ast.Name nodes with these ids will be replaced with corresponding expressions.
|
|
1148
|
+
replace_break
|
|
1149
|
+
replace break statement with another statement.
|
|
1150
|
+
We can't execute for loop on real data here so we can't allow break statements at all.
|
|
1151
|
+
So we will replace them with statements from code after the for loop.
|
|
1152
|
+
If replace_break is Exception then exception will be raised when visiting ast.Break nodes.
|
|
1153
|
+
"""
|
|
1154
|
+
class RewriteName(ast.NodeTransformer):
|
|
1155
|
+
def visit_Name(self, n: ast.Name):
|
|
1156
|
+
return (replace_name or {}).get(n.id) or n
|
|
1157
|
+
|
|
1158
|
+
def visit_Continue(self, n: ast.Continue):
|
|
1159
|
+
# TODO: pass is not continue, we must allow only bodies with one statement in this case
|
|
1160
|
+
return ast.Pass()
|
|
1161
|
+
|
|
1162
|
+
def visit_Break(self, n: ast.Break):
|
|
1163
|
+
if replace_break is None:
|
|
1164
|
+
return n
|
|
1165
|
+
if inspect.isclass(replace_break) and issubclass(replace_break, Exception):
|
|
1166
|
+
raise replace_break("Break is found in for loop and replacer is not provided")
|
|
1167
|
+
if isinstance(replace_break, Exception):
|
|
1168
|
+
raise replace_break
|
|
1169
|
+
assert isinstance(replace_break, ast.stmt)
|
|
1170
|
+
return CaseStatementParser._replace_nodes(replace_break, replace_name=replace_name)
|
|
1171
|
+
|
|
1172
|
+
if not inplace:
|
|
1173
|
+
node = deepcopy(node)
|
|
1174
|
+
RewriteName().visit(node)
|
|
1175
|
+
return node
|
|
1176
|
+
|
|
1177
|
+
def _flatten_for_stmt(self,
|
|
1178
|
+
stmt: ast.For,
|
|
1179
|
+
replace_break: Union[ast.stmt, Exception, Type[Exception], None] = None,
|
|
1180
|
+
stmt_after_for: Optional[ast.stmt] = None) -> List[ast.stmt]:
|
|
1181
|
+
"""
|
|
1182
|
+
Convert for statement to list of copy-pasted statements from the body for each iteration.
|
|
1183
|
+
"""
|
|
1184
|
+
stmts = []
|
|
1185
|
+
target = stmt.target
|
|
1186
|
+
assert isinstance(target, (ast.Name, ast.Tuple)), (
|
|
1187
|
+
f"Unsupported expression '{astunparse(target)}' is used in for statement."
|
|
1188
|
+
" Please, use variable or tuple of variables instead."
|
|
1189
|
+
)
|
|
1190
|
+
if isinstance(target, ast.Tuple):
|
|
1191
|
+
for t in target.elts:
|
|
1192
|
+
assert isinstance(t, ast.Name)
|
|
1193
|
+
targets = target.elts
|
|
1194
|
+
else:
|
|
1195
|
+
targets = [target]
|
|
1196
|
+
replace_name = {}
|
|
1197
|
+
iter_ = self.expression_parser.expression(stmt.iter)
|
|
1198
|
+
for iter_value in iter_.value:
|
|
1199
|
+
if not isinstance(iter_value, Iterable) or isinstance(iter_value, str):
|
|
1200
|
+
iter_value = [iter_value]
|
|
1201
|
+
replace_name = {
|
|
1202
|
+
target.id: ast.Constant(value) # type: ignore[attr-defined]
|
|
1203
|
+
for target, value in zip(targets, iter_value)
|
|
1204
|
+
}
|
|
1205
|
+
for s in stmt.body:
|
|
1206
|
+
stmts.append(
|
|
1207
|
+
self._replace_nodes(s,
|
|
1208
|
+
replace_name=replace_name, # type: ignore[arg-type]
|
|
1209
|
+
replace_break=replace_break)
|
|
1210
|
+
)
|
|
1211
|
+
if stmt_after_for and replace_name:
|
|
1212
|
+
stmts.append(self._replace_nodes(stmt_after_for, replace_name=replace_name)) # type: ignore[arg-type]
|
|
1213
|
+
return stmts
|
|
1214
|
+
|
|
1215
|
+
def _flatten_for_stmts(self, stmts: List[ast.stmt]) -> List[Union[ast.If, ast.Return, ast.Pass]]:
|
|
1216
|
+
"""
|
|
1217
|
+
Find ast.For statements in list of statements and flatten them.
|
|
1218
|
+
Return list of statements without ast.For.
|
|
1219
|
+
Additionally raise exception if unsupported statement is found.
|
|
1220
|
+
"""
|
|
1221
|
+
# TODO: support ast.For statements on deeper levels
|
|
1222
|
+
res_stmts = []
|
|
1223
|
+
for i, stmt in enumerate(stmts):
|
|
1224
|
+
if not isinstance(stmt, (ast.If, ast.Return, ast.For, ast.Pass)):
|
|
1225
|
+
raise ValueError(
|
|
1226
|
+
"this function can't be converted to CASE function, "
|
|
1227
|
+
"only for, if, return and pass statements are allowed"
|
|
1228
|
+
)
|
|
1229
|
+
if isinstance(stmt, ast.For):
|
|
1230
|
+
try:
|
|
1231
|
+
res_stmts.extend(self._flatten_for_stmt(stmt, replace_break=ValueError))
|
|
1232
|
+
except ValueError:
|
|
1233
|
+
stmts_left = len(stmts[i + 1:])
|
|
1234
|
+
assert stmts_left in (0, 1), "Can't be more than one statement after break"
|
|
1235
|
+
replace_break: Union[ast.Return, ast.Pass]
|
|
1236
|
+
if stmts_left == 0:
|
|
1237
|
+
stmt_after_for = None
|
|
1238
|
+
replace_break = ast.Pass()
|
|
1239
|
+
else:
|
|
1240
|
+
stmt_after_for = stmts[i + 1]
|
|
1241
|
+
assert isinstance(stmt_after_for, (ast.Return, ast.Pass)), (
|
|
1242
|
+
'Can only use pass and return statements after for loop with break'
|
|
1243
|
+
)
|
|
1244
|
+
replace_break = stmt_after_for
|
|
1245
|
+
res_stmts.extend(self._flatten_for_stmt(stmt,
|
|
1246
|
+
replace_break=replace_break,
|
|
1247
|
+
stmt_after_for=stmt_after_for))
|
|
1248
|
+
break
|
|
1249
|
+
else:
|
|
1250
|
+
res_stmts.append(stmt)
|
|
1251
|
+
return res_stmts # type: ignore[return-value]
|
|
1252
|
+
|
|
1253
|
+
def _compress_stmts_to_one_stmt(self, stmts: List[ast.stmt], filler=None) -> Union[ast.If, ast.Return]:
|
|
1254
|
+
"""
|
|
1255
|
+
List of if statements will be converted to one if statement.
|
|
1256
|
+
For example:
|
|
1257
|
+
if tick['X'] <= 1:
|
|
1258
|
+
if tick['X'] > 0:
|
|
1259
|
+
return 1
|
|
1260
|
+
else:
|
|
1261
|
+
pass
|
|
1262
|
+
else:
|
|
1263
|
+
if tick['X'] < 3:
|
|
1264
|
+
return 2
|
|
1265
|
+
if tick['X'] <= 3:
|
|
1266
|
+
return 3
|
|
1267
|
+
return 4
|
|
1268
|
+
will be converted to:
|
|
1269
|
+
if tick['X'] <= 1:
|
|
1270
|
+
if tick['X'] > 0:
|
|
1271
|
+
return 1
|
|
1272
|
+
else:
|
|
1273
|
+
if tick['X'] <= 3:
|
|
1274
|
+
return 3
|
|
1275
|
+
else:
|
|
1276
|
+
return 4
|
|
1277
|
+
else:
|
|
1278
|
+
if tick['X'] < 3:
|
|
1279
|
+
return 2
|
|
1280
|
+
else:
|
|
1281
|
+
if tick['X'] <= 3:
|
|
1282
|
+
return 3
|
|
1283
|
+
else:
|
|
1284
|
+
return 4
|
|
1285
|
+
"""
|
|
1286
|
+
filler = filler or ast.Pass()
|
|
1287
|
+
if not stmts:
|
|
1288
|
+
return filler
|
|
1289
|
+
stmt, *others = stmts
|
|
1290
|
+
if isinstance(stmt, ast.Return):
|
|
1291
|
+
return stmt
|
|
1292
|
+
if isinstance(stmt, ast.Pass):
|
|
1293
|
+
return filler
|
|
1294
|
+
filler = self._compress_stmts_to_one_stmt(others, filler=filler)
|
|
1295
|
+
if isinstance(stmt, ast.If):
|
|
1296
|
+
stmt.body = [self._compress_stmts_to_one_stmt(stmt.body, filler=filler)]
|
|
1297
|
+
if stmt.orelse:
|
|
1298
|
+
stmt.orelse = [self._compress_stmts_to_one_stmt(stmt.orelse, filler=filler)]
|
|
1299
|
+
elif filler:
|
|
1300
|
+
stmt.orelse = [filler]
|
|
1301
|
+
assert stmt.orelse
|
|
1302
|
+
return stmt
|
|
1303
|
+
raise ValueError(
|
|
1304
|
+
"this function can't be converted to CASE function, "
|
|
1305
|
+
"only for, if, return and pass statements are allowed"
|
|
1306
|
+
)
|
|
1307
|
+
|
|
1308
|
+
def _replace_local_variables(self, stmts: List[ast.stmt]) -> List[ast.stmt]:
|
|
1309
|
+
"""
|
|
1310
|
+
We support local variables only by calculating their value and
|
|
1311
|
+
replacing all it's occurrences in the code after variable definition.
|
|
1312
|
+
For example:
|
|
1313
|
+
a = 12345
|
|
1314
|
+
if a:
|
|
1315
|
+
return a
|
|
1316
|
+
return 0
|
|
1317
|
+
will be converted to:
|
|
1318
|
+
if 12345:
|
|
1319
|
+
return 12345
|
|
1320
|
+
return 0
|
|
1321
|
+
"""
|
|
1322
|
+
replace_name = {}
|
|
1323
|
+
res_stmts = []
|
|
1324
|
+
for stmt in stmts:
|
|
1325
|
+
if isinstance(stmt, ast.Assign):
|
|
1326
|
+
assert len(stmt.targets) == 1, 'Unpacking local variables is not supported yet'
|
|
1327
|
+
var, val = stmt.targets[0], stmt.value
|
|
1328
|
+
assert isinstance(var, ast.Name)
|
|
1329
|
+
replace_name[var.id] = val
|
|
1330
|
+
continue
|
|
1331
|
+
res_stmts.append(
|
|
1332
|
+
self._replace_nodes(stmt, replace_name=replace_name)
|
|
1333
|
+
)
|
|
1334
|
+
return res_stmts
|
|
1335
|
+
|
|
1336
|
+
def if_stmt(self, stmt: ast.If) -> ast.IfExp:
|
|
1337
|
+
"""
|
|
1338
|
+
Classic if statement with limited set of allowed statements in the body:
|
|
1339
|
+
* only one statement in the body
|
|
1340
|
+
* statement can be return or another if statement with same rules as above
|
|
1341
|
+
|
|
1342
|
+
For example:
|
|
1343
|
+
if tick['X'] > 0:
|
|
1344
|
+
return 'POS'
|
|
1345
|
+
elif tick['X'] == 0:
|
|
1346
|
+
return 'ZERO'
|
|
1347
|
+
else:
|
|
1348
|
+
return 'NEG'
|
|
1349
|
+
will be converted to OneTick's CASE function:
|
|
1350
|
+
CASE(X > 0, 1, 'POS', CASE(X = 0, 1, 'ZERO', 'NEG'))
|
|
1351
|
+
"""
|
|
1352
|
+
# TODO: support many statements in body
|
|
1353
|
+
if len(stmt.body) != 1:
|
|
1354
|
+
raise ValueError("this function can't be converted to CASE function, "
|
|
1355
|
+
"too many statements in if body")
|
|
1356
|
+
body = self.statement(stmt.body[0])
|
|
1357
|
+
|
|
1358
|
+
if len(stmt.orelse) > 1:
|
|
1359
|
+
raise ValueError("this function can't be converted to CASE function, "
|
|
1360
|
+
"too many statements in else body")
|
|
1361
|
+
if stmt.orelse and not isinstance(stmt.orelse[0], ast.Pass):
|
|
1362
|
+
orelse = self.statement(stmt.orelse[0])
|
|
1363
|
+
else:
|
|
1364
|
+
e = self.expression_parser.expression(body)
|
|
1365
|
+
orelse = ast.Constant(ott.default_by_type(ott.get_type_by_objects(e.values)))
|
|
1366
|
+
return ast.IfExp(test=stmt.test, body=body, orelse=orelse)
|
|
1367
|
+
|
|
1368
|
+
def return_stmt(self, stmt: ast.Return) -> ast.expr:
|
|
1369
|
+
"""
|
|
1370
|
+
Return statement.
|
|
1371
|
+
Will be converted to value according to OneTick's syntax.
|
|
1372
|
+
"""
|
|
1373
|
+
if stmt.value is None:
|
|
1374
|
+
raise ValueError('return statement must have value when converting to CASE function')
|
|
1375
|
+
return stmt.value
|
|
1376
|
+
|
|
1377
|
+
def pass_stmt(self, _stmt: ast.Pass) -> ast.Constant:
|
|
1378
|
+
"""
|
|
1379
|
+
Pass statement.
|
|
1380
|
+
Will be converted to None according to OneTick's syntax.
|
|
1381
|
+
"""
|
|
1382
|
+
return ast.Constant(None)
|
|
1383
|
+
|
|
1384
|
+
def compress(self, stmts: List[ast.stmt]) -> Union[ast.If, ast.Return]:
|
|
1385
|
+
"""
|
|
1386
|
+
Compress list of statements to single statement.
|
|
1387
|
+
This is possible only if simple if and return statements are used.
|
|
1388
|
+
"""
|
|
1389
|
+
stmts = self._replace_local_variables(stmts)
|
|
1390
|
+
flat_stmts = self._flatten_for_stmts(stmts)
|
|
1391
|
+
stmt = self._compress_stmts_to_one_stmt(flat_stmts) # type: ignore[arg-type]
|
|
1392
|
+
return stmt
|
|
1393
|
+
|
|
1394
|
+
def statement(self, stmt: ast.stmt) -> ast.expr:
|
|
1395
|
+
"""Return statement converted to expression."""
|
|
1396
|
+
return {
|
|
1397
|
+
ast.If: self.if_stmt,
|
|
1398
|
+
ast.Return: self.return_stmt,
|
|
1399
|
+
ast.Pass: self.pass_stmt,
|
|
1400
|
+
}[type(stmt)](stmt) # type: ignore[operator]
|
|
1401
|
+
|
|
1402
|
+
|
|
1403
|
+
class StatementParser(CaseStatementParser):
|
|
1404
|
+
"""
|
|
1405
|
+
Class with methods to convert ast statements to per tick script lines.
|
|
1406
|
+
"""
|
|
1407
|
+
|
|
1408
|
+
def __init__(self, fun: 'FunctionParser'):
|
|
1409
|
+
super().__init__(fun)
|
|
1410
|
+
self.expression_parser = ExpressionParser(fun=fun) # type: ignore[assignment]
|
|
1411
|
+
self.operator_parser = OperatorParser()
|
|
1412
|
+
self._for_counter = 0
|
|
1413
|
+
|
|
1414
|
+
@staticmethod
|
|
1415
|
+
def _transform_if_expr_to_if_stmt(stmt: Union[ast.Assign, ast.AugAssign]) -> ast.If:
|
|
1416
|
+
"""
|
|
1417
|
+
Per tick script do not support if expressions, so converting it to if statement.
|
|
1418
|
+
For example:
|
|
1419
|
+
tick['X'] = 'A' if tick['S'] > 0 else 'B'
|
|
1420
|
+
will be converted to:
|
|
1421
|
+
if (S > 0) {
|
|
1422
|
+
X = 'A';
|
|
1423
|
+
}
|
|
1424
|
+
else {
|
|
1425
|
+
X = 'B';
|
|
1426
|
+
}
|
|
1427
|
+
"""
|
|
1428
|
+
if not isinstance(stmt.value, ast.IfExp):
|
|
1429
|
+
raise ValueError()
|
|
1430
|
+
if_expr: ast.IfExp = stmt.value
|
|
1431
|
+
body, orelse = deepcopy(stmt), deepcopy(stmt)
|
|
1432
|
+
body.value = if_expr.body
|
|
1433
|
+
orelse.value = if_expr.orelse
|
|
1434
|
+
|
|
1435
|
+
return ast.If(
|
|
1436
|
+
test=if_expr.test,
|
|
1437
|
+
body=[body],
|
|
1438
|
+
orelse=[orelse],
|
|
1439
|
+
)
|
|
1440
|
+
|
|
1441
|
+
def assign(self, stmt: ast.Assign) -> str:
|
|
1442
|
+
"""
|
|
1443
|
+
Assign statement: tick['X'] = 1
|
|
1444
|
+
Will be converted to OneTick syntax: X = 1;
|
|
1445
|
+
"""
|
|
1446
|
+
assert len(stmt.targets) == 1, 'Unpacking variables is not yet supported'
|
|
1447
|
+
|
|
1448
|
+
if isinstance(stmt.value, ast.IfExp):
|
|
1449
|
+
if_stmt = self._transform_if_expr_to_if_stmt(stmt)
|
|
1450
|
+
return self.statement(if_stmt)
|
|
1451
|
+
|
|
1452
|
+
var = self.expression_parser.expression(stmt.targets[0])
|
|
1453
|
+
val = self.expression_parser.expression(stmt.value)
|
|
1454
|
+
|
|
1455
|
+
default_expr = f'{var} = {val};'
|
|
1456
|
+
|
|
1457
|
+
if var.lhs:
|
|
1458
|
+
expr = var.value(val.value)
|
|
1459
|
+
return expr or default_expr
|
|
1460
|
+
|
|
1461
|
+
if var.is_local_variable:
|
|
1462
|
+
var_name = var.value.name
|
|
1463
|
+
if val.is_static:
|
|
1464
|
+
val = Expression(val.value.value)
|
|
1465
|
+
if var_name in self.fun.emulator.STATIC_VARS:
|
|
1466
|
+
raise ValueError(f"Trying to define static variable '{var_name}' more than once")
|
|
1467
|
+
if var_name in self.fun.emulator.LOCAL_VARS:
|
|
1468
|
+
raise ValueError(f"Can't redefine variable '{var_name}' as static")
|
|
1469
|
+
|
|
1470
|
+
if self.fun.emulator.NEW_VALUES:
|
|
1471
|
+
raise ValueError('Mixed definition of static variables and new columns is not supported')
|
|
1472
|
+
|
|
1473
|
+
if val.is_tick:
|
|
1474
|
+
# recreating tick object here, because it doesn't have name yet
|
|
1475
|
+
self.fun.emulator.STATIC_VARS[var_name] = val.dtype(var_name)
|
|
1476
|
+
return f'static {val.value._definition} {var};'
|
|
1477
|
+
self.fun.emulator.STATIC_VARS[var_name] = val.value
|
|
1478
|
+
return f'static {ott.type2str(val.dtype)} {var} = {val};'
|
|
1479
|
+
|
|
1480
|
+
variables = None
|
|
1481
|
+
if var_name in self.fun.emulator.STATIC_VARS:
|
|
1482
|
+
variables = self.fun.emulator.STATIC_VARS
|
|
1483
|
+
elif var_name in self.fun.emulator.LOCAL_VARS:
|
|
1484
|
+
variables = self.fun.emulator.LOCAL_VARS
|
|
1485
|
+
|
|
1486
|
+
if variables is None:
|
|
1487
|
+
if val.is_tick:
|
|
1488
|
+
raise ValueError('Only primitive types are allowed for non static local variables.')
|
|
1489
|
+
if self.fun.emulator.NEW_VALUES:
|
|
1490
|
+
raise ValueError('Mixed definition of local variables and new columns is not supported')
|
|
1491
|
+
self.fun.emulator.LOCAL_VARS[var_name] = val.value
|
|
1492
|
+
return f'{ott.type2str(val.dtype)} {var} = {val};'
|
|
1493
|
+
|
|
1494
|
+
dtype = ott.get_type_by_objects([variables[var_name]])
|
|
1495
|
+
if val.dtype != dtype:
|
|
1496
|
+
raise ValueError(f"Wrong type for variable '{var_name}': should be {dtype}, got {val.dtype}")
|
|
1497
|
+
|
|
1498
|
+
return default_expr
|
|
1499
|
+
|
|
1500
|
+
def aug_assign(self, stmt: ast.AugAssign) -> str:
|
|
1501
|
+
"""
|
|
1502
|
+
Assign with inplace operation statement: tick['X'] += 1.
|
|
1503
|
+
Will be converted to OneTick syntax: X = X + 1;
|
|
1504
|
+
"""
|
|
1505
|
+
target = deepcopy(stmt.target)
|
|
1506
|
+
target.ctx = ast.Load()
|
|
1507
|
+
return self.assign(
|
|
1508
|
+
ast.Assign(
|
|
1509
|
+
targets=[stmt.target],
|
|
1510
|
+
value=ast.BinOp(
|
|
1511
|
+
left=target,
|
|
1512
|
+
op=stmt.op,
|
|
1513
|
+
right=stmt.value,
|
|
1514
|
+
)
|
|
1515
|
+
)
|
|
1516
|
+
)
|
|
1517
|
+
|
|
1518
|
+
def if_stmt(self, stmt: ast.If) -> str: # type: ignore[override]
|
|
1519
|
+
"""
|
|
1520
|
+
Classic if statement:
|
|
1521
|
+
if tick['X'] > 0:
|
|
1522
|
+
tick['Y'] = 1
|
|
1523
|
+
elif tick['X'] == 0:
|
|
1524
|
+
tick['Y'] = 0
|
|
1525
|
+
else:
|
|
1526
|
+
tick['Y'] = -1
|
|
1527
|
+
Will be converted to:
|
|
1528
|
+
if (X > 0) {
|
|
1529
|
+
Y = 1;
|
|
1530
|
+
}
|
|
1531
|
+
else {
|
|
1532
|
+
if (X == 0) {
|
|
1533
|
+
Y = 0;
|
|
1534
|
+
}
|
|
1535
|
+
else {
|
|
1536
|
+
Y = -1;
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
"""
|
|
1540
|
+
test = self.expression_parser.expression(stmt.test)
|
|
1541
|
+
test.convert_to_operation()
|
|
1542
|
+
body = [self.statement(s) for s in stmt.body]
|
|
1543
|
+
orelse = [self.statement(s) for s in stmt.orelse]
|
|
1544
|
+
if test.predefined:
|
|
1545
|
+
if test.value:
|
|
1546
|
+
return '\n'.join(body)
|
|
1547
|
+
return '\n'.join(orelse)
|
|
1548
|
+
lines = []
|
|
1549
|
+
lines.append('if (%s) {' % test)
|
|
1550
|
+
lines.extend(body)
|
|
1551
|
+
lines.append('}')
|
|
1552
|
+
if orelse:
|
|
1553
|
+
lines.append('else {')
|
|
1554
|
+
lines.extend(orelse)
|
|
1555
|
+
lines.append('}')
|
|
1556
|
+
return '\n'.join(lines)
|
|
1557
|
+
|
|
1558
|
+
def return_stmt(self, stmt: ast.Return) -> str: # type: ignore[override]
|
|
1559
|
+
"""
|
|
1560
|
+
Return statement.
|
|
1561
|
+
For now we support returning only boolean values or nothing.
|
|
1562
|
+
Will be converted to: return true;
|
|
1563
|
+
"""
|
|
1564
|
+
# if return is empty then it is not filter
|
|
1565
|
+
v = stmt.value if stmt.value is not None else ast.Constant(value=True)
|
|
1566
|
+
value = self.expression_parser.expression(v)
|
|
1567
|
+
dtype = ott.get_object_type(value.value)
|
|
1568
|
+
if not self.fun.inner_function:
|
|
1569
|
+
if dtype is not bool:
|
|
1570
|
+
raise TypeError(f"Not supported return type {dtype}")
|
|
1571
|
+
if stmt.value is not None:
|
|
1572
|
+
self.fun.returns = True
|
|
1573
|
+
else:
|
|
1574
|
+
assert isinstance(self.fun.ast_node, ast.FunctionDef)
|
|
1575
|
+
msg = (f"Function '{self.fun.ast_node.name}'"
|
|
1576
|
+
f" has return annotation '{self.fun.return_annotation.__name__}',"
|
|
1577
|
+
f" but the type of statement ({astunparse(stmt)}) is '{dtype.__name__}'")
|
|
1578
|
+
try:
|
|
1579
|
+
widest_type = ott.get_type_by_objects([dtype, self.fun.return_annotation])
|
|
1580
|
+
except TypeError as e:
|
|
1581
|
+
raise TypeError(msg) from e
|
|
1582
|
+
if widest_type is not self.fun.return_annotation:
|
|
1583
|
+
raise TypeError(msg)
|
|
1584
|
+
self.fun.returns = True
|
|
1585
|
+
return f'return {value};'
|
|
1586
|
+
|
|
1587
|
+
@staticmethod
|
|
1588
|
+
def _check_break(*nodes) -> bool:
|
|
1589
|
+
"""
|
|
1590
|
+
Check if break statement in the list of nodes (recursively).
|
|
1591
|
+
"""
|
|
1592
|
+
class FoundBreakException(Exception):
|
|
1593
|
+
pass
|
|
1594
|
+
|
|
1595
|
+
class FindBreak(ast.NodeVisitor):
|
|
1596
|
+
def visit_Break(self, n: ast.Break):
|
|
1597
|
+
raise FoundBreakException()
|
|
1598
|
+
|
|
1599
|
+
try:
|
|
1600
|
+
for node in nodes:
|
|
1601
|
+
FindBreak().visit(node)
|
|
1602
|
+
return False
|
|
1603
|
+
except FoundBreakException:
|
|
1604
|
+
return True
|
|
1605
|
+
|
|
1606
|
+
def while_stmt(self, stmt: ast.While) -> str:
|
|
1607
|
+
"""
|
|
1608
|
+
Classic while statement:
|
|
1609
|
+
while tick['X'] > 0:
|
|
1610
|
+
tick['Y'] = 1
|
|
1611
|
+
Will be converted to:
|
|
1612
|
+
while (X > 0) {
|
|
1613
|
+
Y = 1;
|
|
1614
|
+
}
|
|
1615
|
+
"""
|
|
1616
|
+
test = self.expression_parser.expression(stmt.test)
|
|
1617
|
+
test.convert_to_operation()
|
|
1618
|
+
body = [self.statement(s) for s in stmt.body]
|
|
1619
|
+
if test.predefined:
|
|
1620
|
+
is_break_found = self._check_break(*stmt.body)
|
|
1621
|
+
if not is_break_found:
|
|
1622
|
+
raise ValueError(f'The condition of while statement always evaluates to {bool(test.value)}'
|
|
1623
|
+
' and there is no break statement in the loop body.'
|
|
1624
|
+
' That will result in infinite loop.'
|
|
1625
|
+
' Change condition or add break statements.')
|
|
1626
|
+
lines = []
|
|
1627
|
+
lines.append('while (%s) {' % test)
|
|
1628
|
+
lines.extend(body)
|
|
1629
|
+
lines.append('}')
|
|
1630
|
+
return '\n'.join(lines)
|
|
1631
|
+
|
|
1632
|
+
def for_stmt(self, stmt: ast.For) -> str:
|
|
1633
|
+
"""
|
|
1634
|
+
For now for statement in most cases will not be converted to per tick script's for statement.
|
|
1635
|
+
Instead, the statements from the body of the for statement will be duplicated
|
|
1636
|
+
for each iteration.
|
|
1637
|
+
|
|
1638
|
+
For example:
|
|
1639
|
+
for i in [1, 2, 3]:
|
|
1640
|
+
tick['X'] += i
|
|
1641
|
+
will be converted to:
|
|
1642
|
+
X += 1;
|
|
1643
|
+
X += 2;
|
|
1644
|
+
X += 3;
|
|
1645
|
+
|
|
1646
|
+
But simple case with range object will be translated more correctly:
|
|
1647
|
+
For example:
|
|
1648
|
+
for i in range(1, 4):
|
|
1649
|
+
tick['X'] += i
|
|
1650
|
+
will be converted to:
|
|
1651
|
+
for (LOCAL::i = 1; LOCAL::i < 4; LOCAL::i += 1) {
|
|
1652
|
+
X += LOCAL::i;
|
|
1653
|
+
}
|
|
1654
|
+
"""
|
|
1655
|
+
lines = []
|
|
1656
|
+
iter_ = self.expression_parser.expression(stmt.iter)
|
|
1657
|
+
if isinstance(iter_.value, _TickSequence):
|
|
1658
|
+
target = stmt.target
|
|
1659
|
+
assert isinstance(target, ast.Name), "Tuples can't be used while iterating on tick sequences"
|
|
1660
|
+
state_tick = iter_.value._tick_obj(target.id)
|
|
1661
|
+
# TODO: ugly
|
|
1662
|
+
state_tick_name = f"_______state_tick_{self._for_counter}_______"
|
|
1663
|
+
self._for_counter += 1
|
|
1664
|
+
ast_tick = ast.Name(state_tick_name, ctx=ast.Load())
|
|
1665
|
+
lines.append('for (%s %s : %s) {' % (state_tick._definition, state_tick, iter_.value))
|
|
1666
|
+
with self.expression_parser._replace_context(
|
|
1667
|
+
inspect.ClosureVars({}, {state_tick_name: state_tick}, {}, set())
|
|
1668
|
+
):
|
|
1669
|
+
for s in stmt.body:
|
|
1670
|
+
s = self._replace_nodes(s, replace_name={target.id: ast_tick})
|
|
1671
|
+
lines.append(self.statement(s))
|
|
1672
|
+
lines.append('}')
|
|
1673
|
+
elif isinstance(iter_.value, range):
|
|
1674
|
+
target = stmt.target
|
|
1675
|
+
assert isinstance(target, ast.Name), "Tuples can't be used in for loop"
|
|
1676
|
+
var_name = target.id
|
|
1677
|
+
if var_name not in self.fun.emulator.LOCAL_VARS:
|
|
1678
|
+
# initialize counter variable
|
|
1679
|
+
self.fun.emulator.LOCAL_VARS[var_name] = int
|
|
1680
|
+
self.fun.emulator.LOCAL_VARS_NEW_VALUES[var_name].append(0)
|
|
1681
|
+
elif self.fun.emulator.LOCAL_VARS[var_name] is not int:
|
|
1682
|
+
raise ValueError(f'Variable {var_name} was declared before with conflicting type.')
|
|
1683
|
+
counter_var = LocalVariable(var_name, int)
|
|
1684
|
+
counter_var_str = str(counter_var)
|
|
1685
|
+
range_obj = iter_.value
|
|
1686
|
+
start_expr = f'{counter_var_str} = {range_obj.start}'
|
|
1687
|
+
if (range_obj.start < range_obj.stop and range_obj.step <= 0 or
|
|
1688
|
+
range_obj.start > range_obj.stop and range_obj.step >= 0):
|
|
1689
|
+
raise ValueError(f'Range object {range_obj} will result in infinite loop')
|
|
1690
|
+
if range_obj.start < range_obj.stop:
|
|
1691
|
+
condition_expr = f'{counter_var_str} < {range_obj.stop}'
|
|
1692
|
+
else:
|
|
1693
|
+
condition_expr = f'{counter_var_str} > {range_obj.stop}'
|
|
1694
|
+
increment_expr = f'{counter_var_str} += {range_obj.step}'
|
|
1695
|
+
lines.append('for (%s; %s; %s) {' % (start_expr, condition_expr, increment_expr))
|
|
1696
|
+
try:
|
|
1697
|
+
for s in stmt.body:
|
|
1698
|
+
lines.append(self.statement(s))
|
|
1699
|
+
lines.append('}')
|
|
1700
|
+
except Exception:
|
|
1701
|
+
if var_name in self.fun.emulator.LOCAL_VARS_NEW_VALUES:
|
|
1702
|
+
self.fun.emulator.LOCAL_VARS_NEW_VALUES.pop(var_name)
|
|
1703
|
+
self.fun.emulator.LOCAL_VARS.pop(var_name)
|
|
1704
|
+
lines = [self.statement(s) for s in self._flatten_for_stmt(stmt)]
|
|
1705
|
+
else:
|
|
1706
|
+
lines = [self.statement(s) for s in self._flatten_for_stmt(stmt)]
|
|
1707
|
+
return '\n'.join(lines)
|
|
1708
|
+
|
|
1709
|
+
def break_stmt(self, _stmt: ast.Break) -> str:
|
|
1710
|
+
return 'break;'
|
|
1711
|
+
|
|
1712
|
+
def continue_stmt(self, _stmt: ast.Continue) -> str:
|
|
1713
|
+
return 'continue;'
|
|
1714
|
+
|
|
1715
|
+
def pass_stmt(self, stmt: ast.Pass) -> str: # type: ignore[override]
|
|
1716
|
+
"""Pass statement is not converted to anything"""
|
|
1717
|
+
return ''
|
|
1718
|
+
|
|
1719
|
+
def yield_expr(self, expr: ast.Yield) -> Expression:
|
|
1720
|
+
"""
|
|
1721
|
+
Yield expression, like: yield
|
|
1722
|
+
Values for yield are not supported.
|
|
1723
|
+
Will be translated to PROPAGATE_TICK() function.
|
|
1724
|
+
Can be used only as a statement, so this function is here and not in ExpressionParser.
|
|
1725
|
+
"""
|
|
1726
|
+
if expr.value is not None:
|
|
1727
|
+
raise ValueError("Passing value with yield expression is not supported.")
|
|
1728
|
+
return Expression('PROPAGATE_TICK();')
|
|
1729
|
+
|
|
1730
|
+
def expression(self, stmt: ast.Expr) -> str:
|
|
1731
|
+
"""
|
|
1732
|
+
Here goes raw strings and yield expression.
|
|
1733
|
+
For example:
|
|
1734
|
+
if tick['A'] == 0:
|
|
1735
|
+
'return 0;'
|
|
1736
|
+
Here 'return 0;' is used as a statement and an expression.
|
|
1737
|
+
Expression's returned value *must* be a string and
|
|
1738
|
+
this string will be injected in per tick script directly.
|
|
1739
|
+
"""
|
|
1740
|
+
if isinstance(stmt.value, ast.Yield):
|
|
1741
|
+
expression = self.yield_expr(stmt.value)
|
|
1742
|
+
else:
|
|
1743
|
+
expression = self.expression_parser.expression(stmt.value)
|
|
1744
|
+
assert isinstance(expression.value, (str, _Operation)), (
|
|
1745
|
+
f"The statement '{astunparse(stmt)}' can't be used here"
|
|
1746
|
+
" because the value of such statement can be string only"
|
|
1747
|
+
" as it's value will be injected directly in per tick script."
|
|
1748
|
+
)
|
|
1749
|
+
value = str(expression.value)
|
|
1750
|
+
if value and value[-1] != ';':
|
|
1751
|
+
value += ';'
|
|
1752
|
+
return value
|
|
1753
|
+
|
|
1754
|
+
def with_stmt(self, stmt: ast.With) -> str:
|
|
1755
|
+
"""
|
|
1756
|
+
Used only with special context managers. Currently only `_ONCE` is supported.
|
|
1757
|
+
"""
|
|
1758
|
+
if len(stmt.items) != 1:
|
|
1759
|
+
raise ValueError('Currently it is possible to use only one context manager in single with statement')
|
|
1760
|
+
with_item = stmt.items[0]
|
|
1761
|
+
if with_item.optional_vars:
|
|
1762
|
+
raise ValueError('It is not allowed to use "as" in with statements for per-tick script')
|
|
1763
|
+
context_expr = with_item.context_expr
|
|
1764
|
+
if isinstance(context_expr, ast.Call):
|
|
1765
|
+
expr = self.expression_parser.expression(context_expr.func)
|
|
1766
|
+
else:
|
|
1767
|
+
raise ValueError(f'{context_expr} is not called')
|
|
1768
|
+
if not issubclass(expr.value, once):
|
|
1769
|
+
raise ValueError(f'{expr.value} is not supported in per-tick script with statements')
|
|
1770
|
+
return expr.value().get_str('\n'.join([self.statement(s) for s in stmt.body]))
|
|
1771
|
+
|
|
1772
|
+
def statement(self, stmt: ast.stmt) -> str: # type: ignore[override]
|
|
1773
|
+
"""Return parsed statement according to its type."""
|
|
1774
|
+
return {
|
|
1775
|
+
ast.Assign: self.assign,
|
|
1776
|
+
ast.AugAssign: self.aug_assign,
|
|
1777
|
+
ast.If: self.if_stmt,
|
|
1778
|
+
ast.Return: self.return_stmt,
|
|
1779
|
+
ast.While: self.while_stmt,
|
|
1780
|
+
ast.For: self.for_stmt,
|
|
1781
|
+
ast.Break: self.break_stmt,
|
|
1782
|
+
ast.Continue: self.continue_stmt,
|
|
1783
|
+
ast.Pass: self.pass_stmt,
|
|
1784
|
+
ast.Expr: self.expression,
|
|
1785
|
+
ast.With: self.with_stmt,
|
|
1786
|
+
}[type(stmt)](stmt) # type: ignore[operator]
|
|
1787
|
+
|
|
1788
|
+
|
|
1789
|
+
class EndOfBlock(Exception):
|
|
1790
|
+
pass
|
|
1791
|
+
|
|
1792
|
+
|
|
1793
|
+
class LambdaBlockFinder:
|
|
1794
|
+
"""
|
|
1795
|
+
This is simplified version of
|
|
1796
|
+
inspect.BlockFinder
|
|
1797
|
+
that supports multiline lambdas.
|
|
1798
|
+
"""
|
|
1799
|
+
|
|
1800
|
+
OPENING_BRACKETS = {
|
|
1801
|
+
'[': ']',
|
|
1802
|
+
'(': ')',
|
|
1803
|
+
'{': '}',
|
|
1804
|
+
}
|
|
1805
|
+
CLOSING_BRACKETS = {c: o for o, c in OPENING_BRACKETS.items()}
|
|
1806
|
+
BRACKETS_MATCHING = dict(OPENING_BRACKETS.items() | CLOSING_BRACKETS.items())
|
|
1807
|
+
|
|
1808
|
+
def __init__(self):
|
|
1809
|
+
# current indentation level
|
|
1810
|
+
self.indent = 0
|
|
1811
|
+
# row and column index for the start of lambda expression
|
|
1812
|
+
self.start = None
|
|
1813
|
+
# row and column index for the end of lambda expression
|
|
1814
|
+
self.end = None
|
|
1815
|
+
# stack with brackets
|
|
1816
|
+
self.brackets = deque()
|
|
1817
|
+
self.prev = None
|
|
1818
|
+
self.current = None
|
|
1819
|
+
|
|
1820
|
+
def tokeneater(self, type, token, srowcol, erowcol, line, start_row=0):
|
|
1821
|
+
srowcol = (srowcol[0] + start_row, srowcol[1])
|
|
1822
|
+
erowcol = (erowcol[0] + start_row, erowcol[1])
|
|
1823
|
+
self.prev = self.current
|
|
1824
|
+
self.current = tokenize.TokenInfo(type, token, srowcol, erowcol, line)
|
|
1825
|
+
self.end = erowcol
|
|
1826
|
+
if token == 'lambda':
|
|
1827
|
+
self.start = srowcol
|
|
1828
|
+
elif type == tokenize.INDENT:
|
|
1829
|
+
self.indent += 1
|
|
1830
|
+
elif type == tokenize.DEDENT:
|
|
1831
|
+
self.indent -= 1
|
|
1832
|
+
# the end of matching indent/dedent pairs ends a block
|
|
1833
|
+
if self.indent <= 0:
|
|
1834
|
+
raise EndOfBlock
|
|
1835
|
+
elif not self.start:
|
|
1836
|
+
self.indent = 0
|
|
1837
|
+
elif type == tokenize.NEWLINE:
|
|
1838
|
+
if self.indent == 0 or (
|
|
1839
|
+
# if lambda is the argument of the function
|
|
1840
|
+
self.prev and self.prev.type == tokenize.OP and self.prev.string == ','
|
|
1841
|
+
):
|
|
1842
|
+
raise EndOfBlock
|
|
1843
|
+
elif token in self.OPENING_BRACKETS:
|
|
1844
|
+
self.brackets.append(token)
|
|
1845
|
+
elif token in self.CLOSING_BRACKETS:
|
|
1846
|
+
try:
|
|
1847
|
+
assert self.brackets.pop() == self.CLOSING_BRACKETS[token]
|
|
1848
|
+
except (IndexError, AssertionError):
|
|
1849
|
+
self.end = self.prev.end
|
|
1850
|
+
raise EndOfBlock # noqa: W0707
|
|
1851
|
+
|
|
1852
|
+
|
|
1853
|
+
def get_lambda_source(lines):
|
|
1854
|
+
"""Extract the block of lambda code at the top of the given list of lines."""
|
|
1855
|
+
blockfinder = LambdaBlockFinder()
|
|
1856
|
+
start_row = 0
|
|
1857
|
+
while True:
|
|
1858
|
+
try:
|
|
1859
|
+
tokens = tokenize.generate_tokens(iter(lines[start_row:]).__next__)
|
|
1860
|
+
for _token in tokens:
|
|
1861
|
+
blockfinder.tokeneater(*_token, start_row=start_row)
|
|
1862
|
+
break
|
|
1863
|
+
except IndentationError as e:
|
|
1864
|
+
# indentation errors are possible because
|
|
1865
|
+
# we started eating tokens from line with lambda
|
|
1866
|
+
# not from the start of the statement
|
|
1867
|
+
# trying to eat again from the current row in this case
|
|
1868
|
+
start_row = e.args[1][1] - 1
|
|
1869
|
+
continue
|
|
1870
|
+
except EndOfBlock:
|
|
1871
|
+
break
|
|
1872
|
+
start_row, start_column = blockfinder.start
|
|
1873
|
+
end_row, end_column = blockfinder.end
|
|
1874
|
+
# crop block to get rid of tokens from the context around lambda
|
|
1875
|
+
lines = lines[start_row - 1: end_row]
|
|
1876
|
+
lines[-1] = lines[-1][:end_column]
|
|
1877
|
+
lines[0] = lines[0][start_column:]
|
|
1878
|
+
# add brackets around lambda in case it is multiline lambda
|
|
1879
|
+
return ''.join(['(', *lines, ')'])
|
|
1880
|
+
|
|
1881
|
+
|
|
1882
|
+
def is_lambda(lambda_f) -> bool:
|
|
1883
|
+
return isinstance(lambda_f, types.LambdaType) and lambda_f.__name__ == '<lambda>'
|
|
1884
|
+
|
|
1885
|
+
|
|
1886
|
+
def get_source(lambda_f) -> str:
|
|
1887
|
+
"""
|
|
1888
|
+
Get source code of the function or lambda.
|
|
1889
|
+
"""
|
|
1890
|
+
if is_lambda(lambda_f):
|
|
1891
|
+
# that's a hack for multiline lambdas in brackets
|
|
1892
|
+
# inspect.getsource parse them wrong
|
|
1893
|
+
source_lines, lineno = inspect.findsource(lambda_f)
|
|
1894
|
+
if 'lambda' not in source_lines[lineno]:
|
|
1895
|
+
# inspect.findsource fails sometimes too
|
|
1896
|
+
lineno = lambda_f.__code__.co_firstlineno + 1
|
|
1897
|
+
while 'lambda' not in source_lines[lineno]:
|
|
1898
|
+
lineno -= 1
|
|
1899
|
+
source = get_lambda_source(source_lines[lineno:])
|
|
1900
|
+
else:
|
|
1901
|
+
source = inspect.getsource(lambda_f)
|
|
1902
|
+
# doing dedent because self.ast_node do not like indented source code
|
|
1903
|
+
return textwrap.dedent(source)
|
|
1904
|
+
|
|
1905
|
+
|
|
1906
|
+
class FunctionParser:
|
|
1907
|
+
"""
|
|
1908
|
+
Class to parse callable objects (lambdas and functions) to
|
|
1909
|
+
OneTick's per tick script or case functions.
|
|
1910
|
+
Only simple functions corresponding to OneTick syntax supported
|
|
1911
|
+
(without inner functions, importing modules, etc.)
|
|
1912
|
+
You can call simple functions inside,
|
|
1913
|
+
do operations with captured variables (without assigning to them),
|
|
1914
|
+
but using non-pure functions is not recommended because
|
|
1915
|
+
the code in function may not be executed in the order you expect.
|
|
1916
|
+
"""
|
|
1917
|
+
SOURCE_CODE_ATTRIBUTE = '___SOURCE_CODE___'
|
|
1918
|
+
|
|
1919
|
+
def __init__(self, lambda_f, emulator=None, check_arg_name=True, inner_function=False):
|
|
1920
|
+
"""
|
|
1921
|
+
Parameters
|
|
1922
|
+
----------
|
|
1923
|
+
emulator
|
|
1924
|
+
otp.Source emulator that will be tracking changes made to source
|
|
1925
|
+
check_arg_name
|
|
1926
|
+
if True, only callables with zero or one parameter will be allowed.
|
|
1927
|
+
inner_function
|
|
1928
|
+
if True, then function is treated like inner per-tick script function.
|
|
1929
|
+
First argument will be checked and more arguments will be allowed too.
|
|
1930
|
+
"""
|
|
1931
|
+
|
|
1932
|
+
assert isinstance(lambda_f, (types.LambdaType, types.FunctionType, types.MethodType)), (
|
|
1933
|
+
f"It is expected to get a function, method or lambda, but got '{type(lambda_f)}'"
|
|
1934
|
+
)
|
|
1935
|
+
self.lambda_f = lambda_f
|
|
1936
|
+
self.emulator = emulator
|
|
1937
|
+
self.check_arg_name = check_arg_name
|
|
1938
|
+
self.inner_function = inner_function
|
|
1939
|
+
if self.inner_function and not self.check_arg_name:
|
|
1940
|
+
self.check_arg_name = self.check_arg_name or True
|
|
1941
|
+
self.statement_parser = StatementParser(fun=self)
|
|
1942
|
+
self.expression_parser = ExpressionParser(fun=self)
|
|
1943
|
+
self.case_expression_parser = CaseExpressionParser(fun=self)
|
|
1944
|
+
self.case_statement_parser = CaseStatementParser(fun=self)
|
|
1945
|
+
# if the function returns some values or not
|
|
1946
|
+
self.returns = False
|
|
1947
|
+
# will be set to True when args_annotations will be calculated, need it to break recursion
|
|
1948
|
+
self._from_args_annotations = False
|
|
1949
|
+
# calling property here, so we can raise exception as early as possible
|
|
1950
|
+
_ = self.arg_name
|
|
1951
|
+
|
|
1952
|
+
@cached_property
|
|
1953
|
+
def is_method(self) -> bool:
|
|
1954
|
+
return isinstance(self.lambda_f, types.MethodType)
|
|
1955
|
+
|
|
1956
|
+
@cached_property
|
|
1957
|
+
def source_code(self) -> str:
|
|
1958
|
+
"""
|
|
1959
|
+
Get source code of the function or lambda.
|
|
1960
|
+
"""
|
|
1961
|
+
# first try to get code from special attribute else get code the usual way
|
|
1962
|
+
return getattr(self.lambda_f, self.SOURCE_CODE_ATTRIBUTE, None) or get_source(self.lambda_f)
|
|
1963
|
+
|
|
1964
|
+
@cached_property
|
|
1965
|
+
def closure_vars(self) -> inspect.ClosureVars:
|
|
1966
|
+
"""
|
|
1967
|
+
Get closure variables of the function.
|
|
1968
|
+
These are variables that were captured from the context before function definition.
|
|
1969
|
+
For example:
|
|
1970
|
+
A = 12345
|
|
1971
|
+
def a():
|
|
1972
|
+
print(A + 1)
|
|
1973
|
+
In this function variable A is the captured variable.
|
|
1974
|
+
We need closure variables, so we can use them when parsing ast tree.
|
|
1975
|
+
"""
|
|
1976
|
+
return inspect.getclosurevars(self.lambda_f)
|
|
1977
|
+
|
|
1978
|
+
@cached_property
|
|
1979
|
+
def ast_node(self) -> Union[ast.FunctionDef, ast.Lambda]:
|
|
1980
|
+
"""
|
|
1981
|
+
Convert function or lambda to ast module statement.
|
|
1982
|
+
"""
|
|
1983
|
+
source_code = self.source_code
|
|
1984
|
+
tree = ast.parse(source_code)
|
|
1985
|
+
for node in ast.walk(tree):
|
|
1986
|
+
if isinstance(node, (ast.FunctionDef, ast.Lambda)):
|
|
1987
|
+
if isinstance(node, ast.FunctionDef) and ast.get_docstring(node):
|
|
1988
|
+
# remove comment section from function body
|
|
1989
|
+
node.body.pop(0)
|
|
1990
|
+
return node
|
|
1991
|
+
raise ValueError("Can't find function or lambda in source code")
|
|
1992
|
+
|
|
1993
|
+
@cached_property
|
|
1994
|
+
def arg_name(self) -> Optional[str]:
|
|
1995
|
+
"""Get name of the first function or lambda argument."""
|
|
1996
|
+
node = self.ast_node
|
|
1997
|
+
argv = list(node.args.args)
|
|
1998
|
+
argc = len(argv)
|
|
1999
|
+
if argc > 1 and argv[0].arg == 'self' and self.is_method:
|
|
2000
|
+
argv.pop(0)
|
|
2001
|
+
argc -= 1
|
|
2002
|
+
if self.check_arg_name and argc > 1 and not self.inner_function:
|
|
2003
|
+
raise ValueError(
|
|
2004
|
+
"It is allowed to pass only functions or lambdas that take either one or"
|
|
2005
|
+
f" zero parameters, but got {argc}"
|
|
2006
|
+
)
|
|
2007
|
+
arg_name = argv[0].arg if argv else None
|
|
2008
|
+
if isinstance(self.check_arg_name, str) and arg_name != self.check_arg_name:
|
|
2009
|
+
assert isinstance(node, ast.FunctionDef)
|
|
2010
|
+
msg = f"Function '{node.name}' is expected to have first argument named '{self.check_arg_name}'"
|
|
2011
|
+
if arg_name is None:
|
|
2012
|
+
msg += ', but no argument is found'
|
|
2013
|
+
else:
|
|
2014
|
+
msg += f", but argument with name '{arg_name}' is found"
|
|
2015
|
+
raise ValueError(msg)
|
|
2016
|
+
return arg_name
|
|
2017
|
+
|
|
2018
|
+
@cached_property
|
|
2019
|
+
def args_annotations(self) -> dict:
|
|
2020
|
+
node = self.ast_node
|
|
2021
|
+
if not isinstance(node, ast.FunctionDef):
|
|
2022
|
+
return {}
|
|
2023
|
+
argv = list(node.args.args)
|
|
2024
|
+
if argv and argv[0].arg == 'self' and self.is_method:
|
|
2025
|
+
argv.pop(0)
|
|
2026
|
+
if argv and argv[0].arg == self.arg_name:
|
|
2027
|
+
argv.pop(0)
|
|
2028
|
+
if node.args.defaults:
|
|
2029
|
+
raise ValueError("Default values for arguments are not supported"
|
|
2030
|
+
f" in per-tick script function '{node.name}'")
|
|
2031
|
+
annotations = {}
|
|
2032
|
+
for arg in argv:
|
|
2033
|
+
name = arg.arg
|
|
2034
|
+
annotation = getattr(arg, 'annotation', None)
|
|
2035
|
+
if not annotation:
|
|
2036
|
+
raise ValueError(f"Parameter '{name}' in function '{node.name}' doesn't have type annotation")
|
|
2037
|
+
# TODO: remove hacking
|
|
2038
|
+
self._from_args_annotations = True
|
|
2039
|
+
dtype = self.expression_parser.expression(annotation).value
|
|
2040
|
+
self._from_args_annotations = False
|
|
2041
|
+
if not ott.is_type_basic(dtype):
|
|
2042
|
+
raise ValueError(f"Parameter '{name}' in function '{node.name}' has unsupported type: {dtype}")
|
|
2043
|
+
annotations[name] = dtype
|
|
2044
|
+
|
|
2045
|
+
return annotations
|
|
2046
|
+
|
|
2047
|
+
@cached_property
|
|
2048
|
+
def return_annotation(self):
|
|
2049
|
+
node = self.ast_node
|
|
2050
|
+
annotation = getattr(node, 'returns')
|
|
2051
|
+
if not annotation:
|
|
2052
|
+
raise ValueError(f"Function '{node.name}' doesn't have return type annotation")
|
|
2053
|
+
dtype = self.expression_parser.expression(annotation).value
|
|
2054
|
+
if not ott.is_type_basic(dtype):
|
|
2055
|
+
raise ValueError(f"Function '{node.name}' has unsupported return type: {dtype}")
|
|
2056
|
+
return dtype
|
|
2057
|
+
|
|
2058
|
+
def per_tick_script(self) -> str:
|
|
2059
|
+
"""
|
|
2060
|
+
Convert function to OneTick's per tick script.
|
|
2061
|
+
"""
|
|
2062
|
+
node = self.ast_node
|
|
2063
|
+
|
|
2064
|
+
lines = []
|
|
2065
|
+
|
|
2066
|
+
assert isinstance(node, ast.FunctionDef), 'lambdas are not supported in per-tick-script yet'
|
|
2067
|
+
function_def: ast.FunctionDef = node
|
|
2068
|
+
|
|
2069
|
+
for stmt in function_def.body:
|
|
2070
|
+
line = self.statement_parser.statement(stmt)
|
|
2071
|
+
if line:
|
|
2072
|
+
lines.append(line)
|
|
2073
|
+
|
|
2074
|
+
if not self.inner_function and self.returns:
|
|
2075
|
+
# if there were return statement anywhere in the code
|
|
2076
|
+
# then we add default return at the end
|
|
2077
|
+
# TODO: but the default behaviour in OneTick is to propagate all ticks?
|
|
2078
|
+
# changing that will break backward-compatibility
|
|
2079
|
+
lines.append(self.statement_parser.statement(ast.Return(ast.Constant(False))))
|
|
2080
|
+
|
|
2081
|
+
if self.emulator is not None and not self.inner_function:
|
|
2082
|
+
# per tick script syntax demand that we declare variables before using them
|
|
2083
|
+
# so we get all new variables from emulator and declare them.
|
|
2084
|
+
|
|
2085
|
+
def var_definition(key, values):
|
|
2086
|
+
dtype = ott.get_type_by_objects(values)
|
|
2087
|
+
return f'{ott.type2str(dtype)} {str(key)} = {ott.value2str(ott.default_by_type(dtype))};'
|
|
2088
|
+
|
|
2089
|
+
new_columns = []
|
|
2090
|
+
for key, values in self.emulator.NEW_VALUES.items():
|
|
2091
|
+
new_columns.append(var_definition(key, values))
|
|
2092
|
+
new_local_vars = []
|
|
2093
|
+
for key, values in self.emulator.LOCAL_VARS_NEW_VALUES.items():
|
|
2094
|
+
new_local_vars.append(var_definition(LocalVariable(key), values))
|
|
2095
|
+
lines = new_columns + new_local_vars + lines
|
|
2096
|
+
|
|
2097
|
+
if not lines:
|
|
2098
|
+
raise ValueError("The resulted body of PER TICK SCRIPT is empty")
|
|
2099
|
+
|
|
2100
|
+
if not self.inner_function:
|
|
2101
|
+
lines = ['long main() {'] + lines + ['}']
|
|
2102
|
+
for function, *_ in self.emulator.FUNCTIONS.values():
|
|
2103
|
+
lines.append(function)
|
|
2104
|
+
else:
|
|
2105
|
+
if not self.returns:
|
|
2106
|
+
raise ValueError(f"Function '{node.name}' must return values")
|
|
2107
|
+
return_type = ott.type2str(self.return_annotation)
|
|
2108
|
+
args = [
|
|
2109
|
+
f'{ott.type2str(dtype)} {name}'
|
|
2110
|
+
for name, dtype in self.args_annotations.items()
|
|
2111
|
+
]
|
|
2112
|
+
args: str = ', '.join(args) # type: ignore[no-redef]
|
|
2113
|
+
lines = [f'{return_type} {node.name}({args})' + ' {'] + lines + ['}']
|
|
2114
|
+
|
|
2115
|
+
return '\n'.join(lines) + '\n'
|
|
2116
|
+
|
|
2117
|
+
def compress(self) -> ast.expr:
|
|
2118
|
+
"""
|
|
2119
|
+
Convert lambda or function to AST expression.
|
|
2120
|
+
"""
|
|
2121
|
+
node = self.ast_node
|
|
2122
|
+
if isinstance(node, ast.Lambda):
|
|
2123
|
+
return node.body
|
|
2124
|
+
stmt = self.case_statement_parser.compress(node.body)
|
|
2125
|
+
return self.case_statement_parser.statement(stmt)
|
|
2126
|
+
|
|
2127
|
+
def case(self) -> Tuple[str, List[Any]]:
|
|
2128
|
+
"""
|
|
2129
|
+
Convert lambda or function to OneTick's CASE() function.
|
|
2130
|
+
"""
|
|
2131
|
+
expr = self.compress()
|
|
2132
|
+
expr = self.case_expression_parser._convert_bool_op_to_if_expr(expr)
|
|
2133
|
+
expression = self.case_expression_parser.expression(expr)
|
|
2134
|
+
# this will raise type error if type of the expression is not supported
|
|
2135
|
+
ott.default_by_type(ott.get_type_by_objects(expression.values))
|
|
2136
|
+
return str(expression), expression.values
|
|
2137
|
+
|
|
2138
|
+
|
|
2139
|
+
def remote(fun):
|
|
2140
|
+
"""
|
|
2141
|
+
This decorator is needed in case function ``fun``
|
|
2142
|
+
is used in :py:meth:`~onetick.py.Source.apply` method in a `Remote OTP with Ray` context.
|
|
2143
|
+
|
|
2144
|
+
We want to get source code of the function locally
|
|
2145
|
+
because we will not be able to get source code on the remote server.
|
|
2146
|
+
|
|
2147
|
+
See also
|
|
2148
|
+
--------
|
|
2149
|
+
:ref:`Remote OTP with Ray <ray-remote>`.
|
|
2150
|
+
"""
|
|
2151
|
+
# see PY-424
|
|
2152
|
+
@wraps(fun)
|
|
2153
|
+
def wrapper(*args, **kwargs):
|
|
2154
|
+
return fun(*args, **kwargs)
|
|
2155
|
+
setattr(wrapper, FunctionParser.SOURCE_CODE_ATTRIBUTE, get_source(fun))
|
|
2156
|
+
return wrapper
|
|
2157
|
+
|
|
2158
|
+
|
|
2159
|
+
class once:
|
|
2160
|
+
"""
|
|
2161
|
+
Used with a statement or a code block to make it run only once (the first time control reaches to the statement).
|
|
2162
|
+
"""
|
|
2163
|
+
def __enter__(self):
|
|
2164
|
+
# __enter__ and __exit__ methods are only used to express syntax for per-tick script, thus no implementation
|
|
2165
|
+
pass
|
|
2166
|
+
|
|
2167
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
2168
|
+
# __enter__ and __exit__ methods are only used to express syntax for per-tick script, thus no implementation
|
|
2169
|
+
pass
|
|
2170
|
+
|
|
2171
|
+
def get_str(self, string: str) -> str:
|
|
2172
|
+
return f"_ONCE\n{{\n{string}\n}}"
|
|
2173
|
+
|
|
2174
|
+
|
|
2175
|
+
class Once(once):
|
|
2176
|
+
def __init__(self):
|
|
2177
|
+
warnings.warn('Using `otp.Once` is deprecated, please, use `otp.once` instead', FutureWarning)
|
|
2178
|
+
super().__init__()
|
|
2179
|
+
|
|
2180
|
+
|
|
2181
|
+
def logf(message, severity, *args) -> str:
|
|
2182
|
+
"""
|
|
2183
|
+
Call built-in OneTick ``LOGF`` function from per-tick script.
|
|
2184
|
+
|
|
2185
|
+
Parameters
|
|
2186
|
+
----------
|
|
2187
|
+
message: str
|
|
2188
|
+
Log message/format string. The underlying formatting engine is the Boost Format Library:
|
|
2189
|
+
https://www.boost.org/doc/libs/1_53_0/libs/format/doc/format.html
|
|
2190
|
+
|
|
2191
|
+
severity: str
|
|
2192
|
+
Severity of message. Supported values: ``ERROR``, ``WARNING`` and ``INFO``.
|
|
2193
|
+
|
|
2194
|
+
args: list
|
|
2195
|
+
Parameters for format string (optional).
|
|
2196
|
+
|
|
2197
|
+
Returns
|
|
2198
|
+
-------
|
|
2199
|
+
str
|
|
2200
|
+
|
|
2201
|
+
Examples
|
|
2202
|
+
--------
|
|
2203
|
+
>>> t = otp.Ticks({'X': [1, 2, 3]})
|
|
2204
|
+
|
|
2205
|
+
>>> def test_script(tick):
|
|
2206
|
+
... otp.logf("Tick with value X=%1% processed", "INFO", tick["X"])
|
|
2207
|
+
|
|
2208
|
+
>>> t = t.script(test_script)
|
|
2209
|
+
|
|
2210
|
+
See also
|
|
2211
|
+
--------
|
|
2212
|
+
:ref:`Per-Tick Script Guide <python callable parser>`
|
|
2213
|
+
"""
|
|
2214
|
+
|
|
2215
|
+
if severity not in {"ERROR", "WARNING", "INFO"}:
|
|
2216
|
+
raise ValueError(f"Param severity expected to be one of ERROR, WARNING or INFO. Got \"{severity}\"")
|
|
2217
|
+
|
|
2218
|
+
message = ott.value2str(message)
|
|
2219
|
+
severity = ott.value2str(severity)
|
|
2220
|
+
|
|
2221
|
+
if args:
|
|
2222
|
+
params = ", ".join([ott.value2str(arg) for arg in args])
|
|
2223
|
+
return f"LOGF({message}, {severity}, {params});"
|
|
2224
|
+
else:
|
|
2225
|
+
return f"LOGF({message}, {severity});"
|
|
2226
|
+
|
|
2227
|
+
|
|
2228
|
+
def throw_exception(message: str) -> str:
|
|
2229
|
+
"""
|
|
2230
|
+
Call built-in OneTick ``THROW_EXCEPTION`` function from per-tick script.
|
|
2231
|
+
|
|
2232
|
+
Parameters
|
|
2233
|
+
----------
|
|
2234
|
+
message: str
|
|
2235
|
+
Message string that defines the error message to be thrown.
|
|
2236
|
+
|
|
2237
|
+
Returns
|
|
2238
|
+
-------
|
|
2239
|
+
str
|
|
2240
|
+
|
|
2241
|
+
Examples
|
|
2242
|
+
--------
|
|
2243
|
+
>>> t = otp.Ticks({'X': [1, -2, 6]})
|
|
2244
|
+
|
|
2245
|
+
>>> def test_script(tick):
|
|
2246
|
+
... if tick["X"] <= 0:
|
|
2247
|
+
... otp.throw_exception("Tick column X should be greater than zero.")
|
|
2248
|
+
|
|
2249
|
+
>>> t = t.script(test_script)
|
|
2250
|
+
|
|
2251
|
+
See also
|
|
2252
|
+
--------
|
|
2253
|
+
:ref:`Per-Tick Script Guide <python callable parser>`
|
|
2254
|
+
"""
|
|
2255
|
+
message = ott.value2str(message)
|
|
2256
|
+
return f"THROW_EXCEPTION({message});"
|